"""Functional tests for timmy.tools — tool tracking, persona toolkits, catalog. Covers tool usage statistics, persona-to-toolkit mapping, catalog generation, and graceful degradation when Agno is unavailable. """ import pytest from timmy.tools import ( _TOOL_USAGE, PERSONA_TOOLKITS, _track_tool_usage, get_all_available_tools, get_tool_stats, get_tools_for_persona, ) @pytest.fixture(autouse=True) def clear_usage(): """Clear tool usage tracking between tests.""" _TOOL_USAGE.clear() yield _TOOL_USAGE.clear() # ── Tool usage tracking ────────────────────────────────────────────────────── class TestToolTracking: def test_track_creates_agent_entry(self): _track_tool_usage("agent-1", "calculator", success=True) assert "agent-1" in _TOOL_USAGE assert len(_TOOL_USAGE["agent-1"]) == 1 def test_track_records_metadata(self): _track_tool_usage("agent-1", "shell", success=False) entry = _TOOL_USAGE["agent-1"][0] assert entry["tool"] == "shell" assert entry["success"] is False assert "timestamp" in entry def test_track_multiple_calls(self): _track_tool_usage("a1", "search") _track_tool_usage("a1", "read") _track_tool_usage("a1", "search") assert len(_TOOL_USAGE["a1"]) == 3 def test_track_multiple_agents(self): _track_tool_usage("a1", "search") _track_tool_usage("a2", "shell") assert len(_TOOL_USAGE) == 2 class TestGetToolStats: def test_stats_for_specific_agent(self): _track_tool_usage("a1", "search") _track_tool_usage("a1", "read") _track_tool_usage("a1", "search") stats = get_tool_stats("a1") assert stats["agent_id"] == "a1" assert stats["total_calls"] == 3 assert set(stats["tools_used"]) == {"search", "read"} assert len(stats["recent_calls"]) == 3 def test_stats_for_unknown_agent(self): stats = get_tool_stats("nonexistent") assert stats["total_calls"] == 0 assert stats["tools_used"] == [] assert stats["recent_calls"] == [] def test_stats_recent_capped_at_10(self): for i in range(15): _track_tool_usage("a1", f"tool_{i}") stats = get_tool_stats("a1") assert len(stats["recent_calls"]) == 10 def test_stats_all_agents(self): _track_tool_usage("a1", "search") _track_tool_usage("a2", "shell") _track_tool_usage("a2", "read") stats = get_tool_stats() assert "a1" in stats assert "a2" in stats assert stats["a1"]["total_calls"] == 1 assert stats["a2"]["total_calls"] == 2 def test_stats_empty(self): stats = get_tool_stats() assert stats == {} # ── Persona toolkit mapping ────────────────────────────────────────────────── class TestPersonaToolkits: def test_all_expected_personas_present(self): expected = { "echo", "mace", "helm", "seer", "forge", "quill", "lab", "pixel", "lyra", "reel", } assert set(PERSONA_TOOLKITS.keys()) == expected def test_get_tools_for_known_persona_returns_toolkit(self): """Known personas should return a Toolkit with registered tools.""" result = get_tools_for_persona("echo") assert result is not None def test_get_tools_for_unknown_persona(self): result = get_tools_for_persona("nonexistent") assert result is None def test_creative_personas_return_toolkit(self): """Creative personas (pixel, lyra, reel) return toolkits.""" for persona_id in ("pixel", "lyra", "reel"): result = get_tools_for_persona(persona_id) assert result is not None # ── Tool catalog ───────────────────────────────────────────────────────────── class TestToolCatalog: def test_catalog_contains_base_tools(self): catalog = get_all_available_tools() base_tools = { "shell", "python", "read_file", "write_file", "list_files", } for tool_id in base_tools: assert tool_id in catalog, f"Missing base tool: {tool_id}" # web_search removed — dead code, ddgs never installed (#87) assert "web_search" not in catalog def test_catalog_tool_structure(self): catalog = get_all_available_tools() for tool_id, info in catalog.items(): assert "name" in info, f"{tool_id} missing 'name'" assert "description" in info, f"{tool_id} missing 'description'" assert "available_in" in info, f"{tool_id} missing 'available_in'" assert isinstance(info["available_in"], list) def test_catalog_orchestrator_has_all_base_tools(self): catalog = get_all_available_tools() base_tools = { "shell", "python", "read_file", "write_file", "list_files", } for tool_id in base_tools: assert "orchestrator" in catalog[tool_id]["available_in"], ( f"Orchestrator missing tool: {tool_id}" ) def test_catalog_echo_research_tools(self): catalog = get_all_available_tools() assert "echo" in catalog["read_file"]["available_in"] # Echo should NOT have shell assert "echo" not in catalog["shell"]["available_in"] def test_catalog_forge_code_tools(self): catalog = get_all_available_tools() assert "forge" in catalog["shell"]["available_in"] assert "forge" in catalog["python"]["available_in"] assert "forge" in catalog["write_file"]["available_in"] def test_catalog_forge_has_aider(self): """Verify Aider AI tool is available in Forge's toolkit.""" catalog = get_all_available_tools() assert "aider" in catalog assert "forge" in catalog["aider"]["available_in"] assert "orchestrator" in catalog["aider"]["available_in"] class TestAiderTool: """Test the Aider AI coding assistant tool.""" def test_aider_in_tool_catalog(self): """Verify Aider appears in the tool catalog.""" catalog = get_all_available_tools() assert "aider" in catalog assert "forge" in catalog["aider"]["available_in"] class TestFullToolkitConfirmationWarning: """Regression tests for issue #79 — confirmation tool WARNING spam.""" def test_create_full_toolkit_no_confirmation_warning(self, caplog): """create_full_toolkit should not emit 'Requires confirmation tool(s)' warnings. Agno's Toolkit.__init__ validates requires_confirmation_tools against the initial (empty) tool list. We set the attribute *after* construction to avoid the spurious warning while keeping per-tool confirmation checks. """ import logging from timmy.tools import create_full_toolkit with caplog.at_level(logging.WARNING): create_full_toolkit() warning_msgs = [ r.message for r in caplog.records if "Requires confirmation tool" in r.message ] assert warning_msgs == [], f"Unexpected confirmation warnings: {warning_msgs}" def test_dangerous_tools_listed_for_confirmation(self): """After the fix, the toolkit still carries the full DANGEROUS_TOOLS list so Agno can gate execution at runtime.""" from timmy.tool_safety import DANGEROUS_TOOLS from timmy.tools import create_full_toolkit toolkit = create_full_toolkit() if toolkit is None: pytest.skip("Agno tools not available") assert set(toolkit.requires_confirmation_tools) == set(DANGEROUS_TOOLS)