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/dashboard/test_agents_confirmation.py
Alexander Whitestone 9d78eb31d1 ruff (#169)
* polish: streamline nav, extract inline styles, improve tablet UX

- Restructure desktop nav from 8+ flat links + overflow dropdown into
  5 grouped dropdowns (Core, Agents, Intel, System, More) matching
  the mobile menu structure to reduce decision fatigue
- Extract all inline styles from mission_control.html and base.html
  notification elements into mission-control.css with semantic classes
- Replace JS-built innerHTML with secure DOM construction in
  notification loader and chat history
- Add CONNECTING state to connection indicator (amber) instead of
  showing OFFLINE before WebSocket connects
- Add tablet breakpoint (1024px) with larger touch targets for
  Apple Pencil / stylus use and safe-area padding for iPad toolbar
- Add active-link highlighting in desktop dropdown menus
- Rename "Mission Control" page title to "System Overview" to
  disambiguate from the chat home page
- Add "Home — Timmy Time" page title to index.html

https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h

* fix(security): move auth-gate credentials to environment variables

Hardcoded username, password, and HMAC secret in auth-gate.py replaced
with os.environ lookups. Startup now refuses to run if any variable is
unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example.

https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h

* refactor(tooling): migrate from black+isort+bandit to ruff

Replace three separate linting/formatting tools with a single ruff
invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs),
.pre-commit-config.yaml, and CI workflow. Fixes all ruff errors
including unused imports, missing raise-from, and undefined names.
Ruff config maps existing bandit skips to equivalent S-rules.

https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 12:23:35 -04:00

202 lines
7.1 KiB
Python

"""Tests for dashboard tool confirmation flow using native Agno RunOutput."""
from unittest.mock import 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", 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", 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", 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", 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", 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", 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", 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