Files
Timmy-time-dashboard/tests/dashboard/test_agents_confirmation.py
Trip T 78167675f2
All checks were successful
Tests / lint (pull_request) Successful in 3s
Tests / test (pull_request) Successful in 29s
feat: replace custom Gitea client with MCP servers
Replace the bespoke GiteaHand httpx client and tools_gitea.py wrappers
with official MCP tool servers (gitea-mcp + filesystem MCP), wired into
Agno via MCPTools. Switch all session functions to async (arun/acontinue_run)
so MCP tools auto-connect. Delete ~1070 lines of custom Gitea code.

- Create src/timmy/mcp_tools.py with MCP factories + standalone issue bridge
- Wire MCPTools into agent.py tool list (Gitea + filesystem)
- Switch session.py chat/chat_with_tools/continue_chat to async
- Update all callers (dashboard routes, Discord vendor, CLI, thinking engine)
- Add gitea_token fallback from ~/.config/gitea/token
- Add MCP session cleanup to app shutdown hook
- Update tool_safety.py for MCP tool names
- 11 new tests, all 1417 passing, coverage 74.2%

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 21:40:32 -04:00

210 lines
7.3 KiB
Python

"""Tests for dashboard tool confirmation flow using native Agno RunOutput."""
from unittest.mock import AsyncMock, MagicMock, patch
def _mock_completed_run(content="Just a reply."):
"""Create a mock RunOutput for a completed (no tool) run."""
run = MagicMock()
run.content = content
run.status = "COMPLETED"
run.active_requirements = []
return run
def _mock_paused_run(tool_name="shell", tool_args=None, content="Sure, I can do that."):
"""Create a mock RunOutput for a paused run needing tool confirmation."""
tool_args = tool_args or {"command": "echo hello"}
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, req
def _mock_approval_item(item_id="test-approval-123"):
"""Create a mock ApprovalItem."""
item = MagicMock()
item.id = item_id
return item
# ── Chat returns tool actions ────────────────────────────────────────────────
def test_chat_with_tool_call_shows_approval_card(client):
"""When Agno pauses for tool confirmation, the response includes an approval card."""
run, _req = _mock_paused_run()
item = _mock_approval_item()
with (
patch("dashboard.routes.agents.chat_with_tools", new_callable=AsyncMock, return_value=run),
patch("timmy.approvals.create_item", return_value=item),
):
response = client.post("/agents/default/chat", data={"message": "run echo hello"})
assert response.status_code == 200
assert "APPROVE" in response.text
assert "REJECT" in response.text
assert "shell" in response.text
def test_chat_without_tool_call_has_no_approval_card(client):
"""Normal responses without tool calls should not show approval buttons."""
run = _mock_completed_run()
with patch("dashboard.routes.agents.chat_with_tools", return_value=run):
response = client.post("/agents/default/chat", data={"message": "hello"})
assert response.status_code == 200
assert "APPROVE" not in response.text
assert "REJECT" not in response.text
def test_chat_tool_card_contains_impact_badge(client):
"""Tool approval cards should show impact level (shell = high)."""
run, _req = _mock_paused_run()
item = _mock_approval_item()
with (
patch("dashboard.routes.agents.chat_with_tools", new_callable=AsyncMock, return_value=run),
patch("timmy.approvals.create_item", return_value=item),
):
response = client.post("/agents/default/chat", data={"message": "run it"})
assert "impact-high" in response.text
def test_chat_tool_card_has_htmx_approve_endpoint(client):
"""Approval card buttons should target the correct HTMX endpoints."""
run, _req = _mock_paused_run()
item = _mock_approval_item()
with (
patch("dashboard.routes.agents.chat_with_tools", new_callable=AsyncMock, return_value=run),
patch("timmy.approvals.create_item", return_value=item),
):
response = client.post("/agents/default/chat", data={"message": "run it"})
assert 'hx-post="/agents/default/tool/' in response.text
assert '/approve"' in response.text
assert '/reject"' in response.text
# ── Approve endpoint ─────────────────────────────────────────────────────────
def _create_pending_tool(client, approval_id="test-approval-123"):
"""Helper: send a chat that creates a pending tool, return the approval_id."""
run, _req = _mock_paused_run()
item = _mock_approval_item(approval_id)
with (
patch("dashboard.routes.agents.chat_with_tools", new_callable=AsyncMock, return_value=run),
patch("timmy.approvals.create_item", return_value=item),
):
response = client.post("/agents/default/chat", data={"message": "run it"})
assert 'hx-post="/agents/default/tool/' in response.text
return approval_id
def test_approve_executes_tool_and_returns_result(client):
"""Approving a tool should resume via Agno and return the result card."""
approval_id = _create_pending_tool(client)
# Mock the resume returning a completed run with tool result
result_run = MagicMock()
result_tool = MagicMock()
result_tool.tool_name = "shell"
result_tool.result = "hello\n"
result_run.tools = [result_tool]
result_run.content = "Done."
with (
patch(
"dashboard.routes.agents.continue_chat", new_callable=AsyncMock, return_value=result_run
),
patch("timmy.approvals.approve"),
):
response = client.post(f"/agents/default/tool/{approval_id}/approve")
assert response.status_code == 200
assert "hello" in response.text
def test_approve_unknown_id_returns_404(client):
"""Approving a non-existent tool action should return 404."""
response = client.post("/agents/default/tool/nonexistent-id/approve")
assert response.status_code == 404
def test_approve_same_id_twice_returns_404(client):
"""Each approval can only be used once."""
approval_id = _create_pending_tool(client)
result_run = _mock_completed_run("ok")
with (
patch(
"dashboard.routes.agents.continue_chat", new_callable=AsyncMock, return_value=result_run
),
patch("timmy.approvals.approve"),
):
client.post(f"/agents/default/tool/{approval_id}/approve")
# Second attempt should 404
response = client.post(f"/agents/default/tool/{approval_id}/approve")
assert response.status_code == 404
# ── Reject endpoint ──────────────────────────────────────────────────────────
def test_reject_returns_rejected_card(client):
"""Rejecting a tool should return a rejected status card."""
approval_id = _create_pending_tool(client)
with (
patch(
"dashboard.routes.agents.continue_chat",
new_callable=AsyncMock,
return_value=_mock_completed_run(),
),
patch("timmy.approvals.reject"),
):
response = client.post(f"/agents/default/tool/{approval_id}/reject")
assert response.status_code == 200
assert "Rejected" in response.text
def test_reject_unknown_id_still_returns_200(client):
"""Rejecting a non-existent ID is idempotent (no crash)."""
with patch("timmy.approvals.reject"):
response = client.post("/agents/default/tool/nonexistent-id/reject")
assert response.status_code == 200
# ── Safe tools skip confirmation ─────────────────────────────────────────────
def test_safe_tool_does_not_show_approval(client):
"""Completed runs (safe tools auto-executed) should not show approval cards."""
run = _mock_completed_run("Here is the file content.")
with patch("dashboard.routes.agents.chat_with_tools", return_value=run):
response = client.post("/agents/default/chat", data={"message": "read the readme"})
assert "APPROVE" not in response.text
assert "REJECT" not in response.text