forked from Rockachopa/Timmy-time-dashboard
* 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>
114 lines
3.9 KiB
Python
114 lines
3.9 KiB
Python
"""Tests for timmy.tool_safety — classification, extraction, and formatting."""
|
|
|
|
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"
|