forked from Rockachopa/Timmy-time-dashboard
Replace the homebrew regex-based tool extraction and manual dispatch (tool_executor.py) with Agno's built-in Human-In-The-Loop confirmation: - Toolkit(requires_confirmation_tools=...) marks dangerous tools - agent.run() returns RunOutput with status=paused when confirmation needed - RunRequirement.confirm()/reject() + agent.continue_run() resumes execution Dashboard and Discord vendor both use the native flow. DuckDuckGo import isolated so its absence doesn't kill all tools. Test stubs cleaned up (agno is a real dependency, only truly optional packages stubbed). 1384 tests pass in parallel (~14s). Co-authored-by: Trip T <trip@local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
116 lines
3.9 KiB
Python
116 lines
3.9 KiB
Python
"""Tests for timmy.tool_safety — classification, extraction, and formatting."""
|
|
|
|
import pytest
|
|
|
|
from timmy.tool_safety import (
|
|
extract_tool_calls,
|
|
format_action_description,
|
|
get_impact_level,
|
|
requires_confirmation,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# requires_confirmation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRequiresConfirmation:
|
|
def test_dangerous_tools(self):
|
|
for tool in ("shell", "python", "write_file", "aider", "plan_and_execute"):
|
|
assert requires_confirmation(tool) is True
|
|
|
|
def test_safe_tools(self):
|
|
for tool in ("web_search", "calculator", "read_file", "list_files"):
|
|
assert requires_confirmation(tool) is False
|
|
|
|
def test_unknown_defaults_to_dangerous(self):
|
|
assert requires_confirmation("totally_unknown") is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# extract_tool_calls
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExtractToolCalls:
|
|
def test_arguments_format(self):
|
|
text = (
|
|
'Creating dir. {"name": "shell", "arguments": {"args": ["mkdir", "-p", "/tmp/test"]}}'
|
|
)
|
|
calls = extract_tool_calls(text)
|
|
assert len(calls) == 1
|
|
assert calls[0][0] == "shell"
|
|
assert calls[0][1]["args"] == ["mkdir", "-p", "/tmp/test"]
|
|
|
|
def test_parameters_format(self):
|
|
text = 'Result: {"name": "python", "parameters": {"code": "print(1+1)"}}'
|
|
calls = extract_tool_calls(text)
|
|
assert len(calls) == 1
|
|
assert calls[0][0] == "python"
|
|
|
|
def test_multiple_calls(self):
|
|
text = (
|
|
'Step 1: {"name": "shell", "arguments": {"args": ["mkdir", "/tmp/a"]}} '
|
|
'Step 2: {"name": "write_file", "arguments": {"file_name": "/tmp/a/f.md", "contents": "hi"}}'
|
|
)
|
|
calls = extract_tool_calls(text)
|
|
assert len(calls) == 2
|
|
|
|
def test_empty_and_none(self):
|
|
assert extract_tool_calls("") == []
|
|
assert extract_tool_calls(None) == []
|
|
assert extract_tool_calls("Just normal text.") == []
|
|
|
|
def test_malformed_json(self):
|
|
text = '{"name": "shell", "arguments": {not valid json}}'
|
|
assert extract_tool_calls(text) == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# format_action_description
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestFormatActionDescription:
|
|
def test_shell_command(self):
|
|
desc = format_action_description("shell", {"command": "ls -la /tmp"})
|
|
assert "ls -la /tmp" in desc
|
|
|
|
def test_shell_args_list(self):
|
|
desc = format_action_description("shell", {"args": ["mkdir", "-p", "/tmp/t"]})
|
|
assert "mkdir -p /tmp/t" in desc
|
|
|
|
def test_write_file(self):
|
|
desc = format_action_description(
|
|
"write_file", {"file_name": "/tmp/f.md", "contents": "hello world"}
|
|
)
|
|
assert "/tmp/f.md" in desc
|
|
assert "11 chars" in desc
|
|
|
|
def test_python(self):
|
|
desc = format_action_description("python", {"code": "print(42)"})
|
|
assert "print(42)" in desc
|
|
|
|
def test_unknown_tool(self):
|
|
desc = format_action_description("custom_tool", {"key": "value"})
|
|
assert "custom_tool" in desc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_impact_level
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetImpactLevel:
|
|
def test_high(self):
|
|
assert get_impact_level("shell") == "high"
|
|
assert get_impact_level("python") == "high"
|
|
|
|
def test_medium(self):
|
|
assert get_impact_level("write_file") == "medium"
|
|
assert get_impact_level("aider") == "medium"
|
|
|
|
def test_low(self):
|
|
assert get_impact_level("web_search") == "low"
|
|
assert get_impact_level("unknown") == "low"
|