forked from Rockachopa/Timmy-time-dashboard
Give Timmy the ability to file Gitea issues when he notices bugs, stale state, or improvement opportunities in his own codebase. Components: - GiteaHand async API client (infrastructure/hands/gitea.py) - Token auth with ~/.config/gitea/token fallback - Create/list/close issues, dedup by title similarity - Graceful degradation when Gitea unreachable - Tool functions (timmy/tools_gitea.py) - create_gitea_issue: file issues with dedup + work order bridge - list_gitea_issues: check existing backlog - Classified as SAFE (no confirmation needed) - Thinking post-hook (_maybe_file_issues in thinking.py) - Every 20 thoughts, LLM classifies recent thoughts for actionable items - Auto-files bugs/improvements to Gitea with dedup - Bridges to local work order system for dashboard tracking - Config: gitea_url, gitea_token, gitea_repo, gitea_enabled, gitea_timeout, thinking_issue_every All 1426 tests pass, 74.17% coverage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
216 lines
6.7 KiB
Python
216 lines
6.7 KiB
Python
"""Tests for Gitea tool functions.
|
|
|
|
Covers:
|
|
- create_gitea_issue tool (success, dedup skip, unavailable)
|
|
- list_gitea_issues tool (success, empty, unavailable)
|
|
- Work order bridge
|
|
- Tool safety classification
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tool safety classification
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_gitea_tools_are_safe():
|
|
"""Gitea tools should be classified as safe (no confirmation needed)."""
|
|
from timmy.tool_safety import requires_confirmation
|
|
|
|
assert requires_confirmation("create_gitea_issue") is False
|
|
assert requires_confirmation("list_gitea_issues") is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# create_gitea_issue
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# All patches target infrastructure.hands.gitea.gitea_hand because
|
|
# tools_gitea.py uses deferred imports inside function bodies.
|
|
|
|
|
|
def test_create_issue_unavailable():
|
|
"""Should return message when Gitea is not configured."""
|
|
mock_hand = MagicMock()
|
|
mock_hand.available = False
|
|
|
|
with patch("infrastructure.hands.gitea.gitea_hand", mock_hand):
|
|
from timmy.tools_gitea import create_gitea_issue
|
|
|
|
result = create_gitea_issue("Test issue", "Body")
|
|
assert "not configured" in result
|
|
|
|
|
|
def test_create_issue_success():
|
|
"""Should create issue and return confirmation."""
|
|
from infrastructure.hands.gitea import GiteaResult
|
|
|
|
mock_hand = MagicMock()
|
|
mock_hand.available = True
|
|
mock_hand.find_duplicate = AsyncMock(return_value=None)
|
|
mock_hand.create_issue = AsyncMock(
|
|
return_value=GiteaResult(
|
|
operation="POST",
|
|
success=True,
|
|
data={"number": 42, "html_url": "http://localhost:3000/issues/42"},
|
|
)
|
|
)
|
|
|
|
with (
|
|
patch("infrastructure.hands.gitea.gitea_hand", mock_hand),
|
|
patch("timmy.tools_gitea._bridge_to_work_order"),
|
|
):
|
|
from timmy.tools_gitea import create_gitea_issue
|
|
|
|
result = create_gitea_issue("Test bug", "Bug description", "bug")
|
|
assert "#42" in result
|
|
assert "Test bug" in result
|
|
|
|
|
|
def test_create_issue_dedup_skip():
|
|
"""Should skip when similar issue already exists."""
|
|
mock_hand = MagicMock()
|
|
mock_hand.available = True
|
|
mock_hand.find_duplicate = AsyncMock(
|
|
return_value={"number": 10, "html_url": "http://localhost:3000/issues/10"}
|
|
)
|
|
|
|
with patch("infrastructure.hands.gitea.gitea_hand", mock_hand):
|
|
from timmy.tools_gitea import create_gitea_issue
|
|
|
|
result = create_gitea_issue("Existing issue")
|
|
assert "Skipped" in result
|
|
assert "#10" in result
|
|
|
|
|
|
def test_create_issue_api_failure():
|
|
"""Should return error message on API failure."""
|
|
from infrastructure.hands.gitea import GiteaResult
|
|
|
|
mock_hand = MagicMock()
|
|
mock_hand.available = True
|
|
mock_hand.find_duplicate = AsyncMock(return_value=None)
|
|
mock_hand.create_issue = AsyncMock(
|
|
return_value=GiteaResult(
|
|
operation="POST",
|
|
success=False,
|
|
error="HTTP 500: Internal Server Error",
|
|
)
|
|
)
|
|
|
|
with (
|
|
patch("infrastructure.hands.gitea.gitea_hand", mock_hand),
|
|
patch("timmy.tools_gitea._bridge_to_work_order"),
|
|
):
|
|
from timmy.tools_gitea import create_gitea_issue
|
|
|
|
result = create_gitea_issue("Test issue")
|
|
assert "Failed" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# list_gitea_issues
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_list_issues_unavailable():
|
|
"""Should return message when Gitea is not configured."""
|
|
mock_hand = MagicMock()
|
|
mock_hand.available = False
|
|
|
|
with patch("infrastructure.hands.gitea.gitea_hand", mock_hand):
|
|
from timmy.tools_gitea import list_gitea_issues
|
|
|
|
result = list_gitea_issues()
|
|
assert "not configured" in result
|
|
|
|
|
|
def test_list_issues_success():
|
|
"""Should return formatted issue list."""
|
|
from infrastructure.hands.gitea import GiteaResult
|
|
|
|
mock_hand = MagicMock()
|
|
mock_hand.available = True
|
|
mock_hand.list_issues = AsyncMock(
|
|
return_value=GiteaResult(
|
|
operation="GET",
|
|
success=True,
|
|
data=[
|
|
{"number": 1, "title": "Bug fix", "labels": [{"name": "bug"}]},
|
|
{"number": 2, "title": "Feature request", "labels": []},
|
|
],
|
|
)
|
|
)
|
|
|
|
with patch("infrastructure.hands.gitea.gitea_hand", mock_hand):
|
|
from timmy.tools_gitea import list_gitea_issues
|
|
|
|
result = list_gitea_issues("open")
|
|
assert "#1" in result
|
|
assert "Bug fix" in result
|
|
assert "[bug]" in result
|
|
assert "#2" in result
|
|
|
|
|
|
def test_list_issues_empty():
|
|
"""Should return empty message when no issues."""
|
|
from infrastructure.hands.gitea import GiteaResult
|
|
|
|
mock_hand = MagicMock()
|
|
mock_hand.available = True
|
|
mock_hand.list_issues = AsyncMock(
|
|
return_value=GiteaResult(
|
|
operation="GET",
|
|
success=True,
|
|
data=[],
|
|
)
|
|
)
|
|
|
|
with patch("infrastructure.hands.gitea.gitea_hand", mock_hand):
|
|
from timmy.tools_gitea import list_gitea_issues
|
|
|
|
result = list_gitea_issues()
|
|
assert "No open issues" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Work order bridge
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_bridge_to_work_order(tmp_path):
|
|
"""Should create a work order in the local database."""
|
|
from timmy.tools_gitea import _bridge_to_work_order
|
|
|
|
# Point to a "data" subdir inside tmp_path so the code creates it
|
|
data_dir = tmp_path / "data"
|
|
data_dir.mkdir()
|
|
db_path = data_dir / "work_orders.db"
|
|
|
|
with patch("timmy.tools_gitea.settings") as mock_settings:
|
|
mock_settings.repo_root = str(tmp_path)
|
|
_bridge_to_work_order("Test WO", "Description", "bug")
|
|
|
|
import sqlite3
|
|
|
|
conn = sqlite3.connect(str(db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
rows = conn.execute("SELECT * FROM work_orders").fetchall()
|
|
conn.close()
|
|
|
|
assert len(rows) == 1
|
|
assert rows[0]["title"] == "Test WO"
|
|
assert rows[0]["submitter"] == "timmy-thinking"
|
|
assert rows[0]["category"] == "bug"
|
|
|
|
|
|
def test_bridge_to_work_order_graceful_failure():
|
|
"""Should not raise when bridge fails."""
|
|
from timmy.tools_gitea import _bridge_to_work_order
|
|
|
|
with patch("timmy.tools_gitea.settings") as mock_settings:
|
|
mock_settings.repo_root = "/nonexistent/path/that/cannot/exist"
|
|
# Should not raise
|
|
_bridge_to_work_order("Test", "Desc", "bug")
|