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>
122 lines
4.0 KiB
Python
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
|