This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/tests/integrations/test_discord_confirmation.py
Kimi Agent b3a1e0ce36 fix: prune dead web_search tool — ddgs never installed (#87)
Remove DuckDuckGoTools import, all web_search registrations across 4 toolkit
factories, catalog entry, safety classification, prompt references, and
session regex. Total: -41 lines of dead code.

consult_grok is functional (grok_enabled=True, API key set) and opt-in,
so it stays — but Timmy never calls it autonomously, which is correct
sovereign behavior (no cloud calls unless user permits).

Closes #87
2026-03-14 18:13:51 -04:00

275 lines
9.4 KiB
Python

"""Tests for Discord action confirmation system using native Agno RunOutput.
Covers tool safety classification, formatting, impact levels,
and the confirmation flow in _handle_message.
"""
from unittest.mock import AsyncMock, MagicMock
import pytest
# ---------------------------------------------------------------------------
# _format_action_description (imported from tool_safety)
# ---------------------------------------------------------------------------
class TestFormatActionDescription:
def test_shell_command_string(self):
from integrations.chat_bridge.vendors.discord import _format_action_description
desc = _format_action_description("shell", {"command": "ls -la /tmp"})
assert "ls -la /tmp" in desc
def test_shell_command_list(self):
from integrations.chat_bridge.vendors.discord import _format_action_description
desc = _format_action_description("shell", {"args": ["mkdir", "-p", "/tmp/test"]})
assert "mkdir -p /tmp/test" in desc
def test_write_file(self):
from integrations.chat_bridge.vendors.discord import _format_action_description
desc = _format_action_description(
"write_file", {"file_name": "/tmp/foo.md", "contents": "hello world"}
)
assert "/tmp/foo.md" in desc
assert "11 chars" in desc
def test_python_code(self):
from integrations.chat_bridge.vendors.discord import _format_action_description
desc = _format_action_description("python", {"code": "print(42)"})
assert "print(42)" in desc
def test_unknown_tool(self):
from integrations.chat_bridge.vendors.discord import _format_action_description
desc = _format_action_description("custom_tool", {"key": "value"})
assert "custom_tool" in desc
# ---------------------------------------------------------------------------
# _get_impact_level (imported from tool_safety)
# ---------------------------------------------------------------------------
class TestGetImpactLevel:
def test_high_impact(self):
from integrations.chat_bridge.vendors.discord import _get_impact_level
assert _get_impact_level("shell") == "high"
assert _get_impact_level("python") == "high"
def test_medium_impact(self):
from integrations.chat_bridge.vendors.discord import _get_impact_level
assert _get_impact_level("write_file") == "medium"
assert _get_impact_level("aider") == "medium"
def test_low_impact(self):
from integrations.chat_bridge.vendors.discord import _get_impact_level
assert _get_impact_level("calculator") == "low"
assert _get_impact_level("unknown") == "low"
# ---------------------------------------------------------------------------
# Tool safety classification
# ---------------------------------------------------------------------------
class TestToolSafety:
def test_shell_requires_confirmation(self):
from timmy.tool_safety import requires_confirmation
assert requires_confirmation("shell") is True
def test_python_requires_confirmation(self):
from timmy.tool_safety import requires_confirmation
assert requires_confirmation("python") is True
def test_write_file_requires_confirmation(self):
from timmy.tool_safety import requires_confirmation
assert requires_confirmation("write_file") is True
def test_read_file_is_safe(self):
from timmy.tool_safety import requires_confirmation
assert requires_confirmation("read_file") is False
def test_calculator_is_safe(self):
from timmy.tool_safety import requires_confirmation
assert requires_confirmation("calculator") is False
def test_memory_search_is_safe(self):
from timmy.tool_safety import requires_confirmation
assert requires_confirmation("memory_search") is False
def test_unknown_tool_requires_confirmation(self):
from timmy.tool_safety import requires_confirmation
assert requires_confirmation("unknown_tool") is True
# ---------------------------------------------------------------------------
# _handle_message confirmation flow (native Agno RunOutput)
# ---------------------------------------------------------------------------
def _mock_paused_run(tool_name="shell", tool_args=None, content="I will create the dir."):
"""Create a mock RunOutput for a paused run needing tool confirmation."""
tool_args = tool_args or {"args": ["mkdir", "/tmp/test"]}
te = MagicMock()
te.tool_name = tool_name
te.tool_args = tool_args
req = MagicMock()
req.needs_confirmation = True
req.tool_execution = te
run = MagicMock()
run.content = content
run.status = "PAUSED"
run.active_requirements = [req]
return run
def _mock_completed_run(content="Hello! How can I help?"):
"""Create a mock RunOutput for a completed (no tool) run."""
run = MagicMock()
run.content = content
run.status = "COMPLETED"
run.active_requirements = []
return run
class TestHandleMessageConfirmation:
@pytest.mark.asyncio
async def test_dangerous_tool_sends_confirmation(self, monkeypatch):
"""When Agno pauses for tool confirmation, should send confirmation prompt."""
from integrations.chat_bridge.vendors.discord import DiscordVendor
vendor = DiscordVendor()
# Mock chat_with_tools returning a paused RunOutput
paused_run = _mock_paused_run()
monkeypatch.setattr(
"integrations.chat_bridge.vendors.discord.chat_with_tools",
AsyncMock(return_value=paused_run),
)
vendor._client = MagicMock()
vendor._client.user = MagicMock()
vendor._client.user.id = 12345
message = MagicMock()
message.content = "create a directory"
message.channel = MagicMock()
message.channel.guild = MagicMock()
monkeypatch.setattr(vendor, "_get_or_create_thread", AsyncMock(return_value=None))
ctx = AsyncMock()
ctx.__aenter__ = AsyncMock(return_value=None)
ctx.__aexit__ = AsyncMock(return_value=False)
message.channel.typing = MagicMock(return_value=ctx)
message.channel.send = AsyncMock()
# Mock approvals
mock_item = MagicMock()
mock_item.id = "test-approval-id-1234"
monkeypatch.setattr(
"timmy.approvals.create_item",
lambda **kwargs: mock_item,
)
vendor._send_confirmation = AsyncMock()
await vendor._handle_message(message)
# Should have called _send_confirmation for the shell tool
vendor._send_confirmation.assert_called_once()
call_args = vendor._send_confirmation.call_args
assert call_args[0][1] == "shell" # tool_name
assert call_args[0][3] == "test-approval-id-1234" # approval_id
@pytest.mark.asyncio
async def test_no_tool_calls_sends_normal_response(self, monkeypatch):
"""When Agno returns a completed run, should send text directly."""
from integrations.chat_bridge.vendors.discord import DiscordVendor
vendor = DiscordVendor()
completed_run = _mock_completed_run()
monkeypatch.setattr(
"integrations.chat_bridge.vendors.discord.chat_with_tools",
AsyncMock(return_value=completed_run),
)
vendor._client = MagicMock()
vendor._client.user = MagicMock()
vendor._client.user.id = 12345
message = MagicMock()
message.content = "hello"
message.channel = MagicMock()
message.channel.guild = MagicMock()
monkeypatch.setattr(vendor, "_get_or_create_thread", AsyncMock(return_value=None))
ctx = AsyncMock()
ctx.__aenter__ = AsyncMock(return_value=None)
ctx.__aexit__ = AsyncMock(return_value=False)
message.channel.typing = MagicMock(return_value=ctx)
message.channel.send = AsyncMock()
await vendor._handle_message(message)
# Should send the text response directly (no confirmation)
message.channel.send.assert_called()
sent_text = message.channel.send.call_args_list[-1][0][0]
assert "Hello" in sent_text
@pytest.mark.asyncio
async def test_confirmation_disabled_via_config(self, monkeypatch):
"""When discord_confirm_actions=False, no confirmation prompts sent."""
from config import settings
from integrations.chat_bridge.vendors.discord import DiscordVendor
monkeypatch.setattr(settings, "discord_confirm_actions", False)
vendor = DiscordVendor()
paused_run = _mock_paused_run()
monkeypatch.setattr(
"integrations.chat_bridge.vendors.discord.chat_with_tools",
AsyncMock(return_value=paused_run),
)
vendor._client = MagicMock()
vendor._client.user = MagicMock()
vendor._client.user.id = 12345
message = MagicMock()
message.content = "do something"
message.channel = MagicMock()
message.channel.guild = MagicMock()
monkeypatch.setattr(vendor, "_get_or_create_thread", AsyncMock(return_value=None))
ctx = AsyncMock()
ctx.__aenter__ = AsyncMock(return_value=None)
ctx.__aexit__ = AsyncMock(return_value=False)
message.channel.typing = MagicMock(return_value=ctx)
message.channel.send = AsyncMock()
vendor._send_confirmation = AsyncMock()
await vendor._handle_message(message)
# Should NOT call _send_confirmation
vendor._send_confirmation.assert_not_called()