Files
Timmy-time-dashboard/tests/timmy/test_artifacts.py
kimi 80b6e40db0
All checks were successful
Tests / lint (pull_request) Successful in 6s
Tests / test (pull_request) Successful in 1m8s
feat: add artifacts module — give Timmy hands to create notes, decisions, and issues
Adds timmy.artifacts with three async functions:
- save_note(): write markdown to ~/.timmy/notes/
- append_decision(): append to ~/.timmy/decisions.md
- create_issue(): file Gitea issues with fallback to local notes

Fixes #326

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 20:18:18 -04:00

122 lines
4.0 KiB
Python

"""Tests for timmy.artifacts — note saving, decision log, issue creation."""
from __future__ import annotations
from unittest.mock import AsyncMock, patch
import pytest
@pytest.fixture(autouse=True)
def _temp_timmy_home(tmp_path, monkeypatch):
"""Redirect ~/.timmy to a temp directory for all tests."""
monkeypatch.setattr("timmy.artifacts.TIMMY_HOME", tmp_path)
monkeypatch.setattr("timmy.artifacts.NOTES_DIR", tmp_path / "notes")
monkeypatch.setattr("timmy.artifacts.DECISION_LOG", tmp_path / "decisions.md")
class TestSlugify:
def test_basic(self):
from timmy.artifacts import _slugify
assert _slugify("API Design Notes") == "api-design-notes"
def test_special_chars(self):
from timmy.artifacts import _slugify
assert _slugify("foo/bar: baz!") == "foo-bar-baz"
def test_empty(self):
from timmy.artifacts import _slugify
assert _slugify("") == "untitled"
def test_truncation(self):
from timmy.artifacts import _slugify
result = _slugify("a" * 200)
assert len(result) <= 80
class TestSaveNote:
async def test_creates_file(self, tmp_path):
from timmy.artifacts import save_note
path = await save_note("Test Note", "Hello world")
assert path.exists()
text = path.read_text()
assert "# Test Note" in text
assert "Hello world" in text
assert path.parent == tmp_path / "notes"
async def test_filename_has_slug(self, tmp_path):
from timmy.artifacts import save_note
path = await save_note("My Cool Idea", "content")
assert "my-cool-idea" in path.name
assert path.suffix == ".md"
class TestAppendDecision:
async def test_creates_log_if_missing(self, tmp_path):
from timmy.artifacts import append_decision
path = await append_decision("Use REST over GraphQL")
assert path.exists()
text = path.read_text()
assert "# Timmy Decision Log" in text
assert "Use REST over GraphQL" in text
async def test_appends_to_existing(self, tmp_path):
from timmy.artifacts import append_decision
await append_decision("First decision")
await append_decision("Second decision")
text = (tmp_path / "decisions.md").read_text()
assert "First decision" in text
assert "Second decision" in text
class TestCreateIssue:
async def test_fallback_when_gitea_disabled(self, tmp_path, monkeypatch):
from timmy.artifacts import create_issue
monkeypatch.setattr("timmy.artifacts.settings.gitea_enabled", False)
result = await create_issue("Bug title", "Bug body")
assert "saved as note" in result.lower()
notes = list((tmp_path / "notes").glob("*.md"))
assert len(notes) == 1
assert "Bug title" in notes[0].read_text()
async def test_delegates_to_mcp(self, monkeypatch):
from timmy.artifacts import create_issue
monkeypatch.setattr("timmy.artifacts.settings.gitea_enabled", True)
monkeypatch.setattr("timmy.artifacts.settings.gitea_token", "fake-token")
mock_mcp = AsyncMock(return_value="Created issue: Test")
with patch("timmy.mcp_tools.create_gitea_issue_via_mcp", mock_mcp):
result = await create_issue("Test", "body", "bug")
assert "Created issue" in result
mock_mcp.assert_awaited_once_with("Test", "body", "bug")
async def test_mcp_error_falls_back_to_note(self, tmp_path, monkeypatch):
from timmy.artifacts import create_issue
monkeypatch.setattr("timmy.artifacts.settings.gitea_enabled", True)
monkeypatch.setattr("timmy.artifacts.settings.gitea_token", "fake-token")
mock_mcp = AsyncMock(side_effect=RuntimeError("connection refused"))
with patch("timmy.mcp_tools.create_gitea_issue_via_mcp", mock_mcp):
result = await create_issue("Broken thing", "details")
assert "saved as note" in result.lower()
notes = list((tmp_path / "notes").glob("*.md"))
assert len(notes) == 1