"""Tests for timmy.approvals — approval workflow and Golden Timmy rule.""" from datetime import UTC, datetime, timedelta import pytest from timmy.approvals import ( GOLDEN_TIMMY, ApprovalItem, _get_conn, approve, create_item, expire_old, get_item, list_all, list_pending, reject, ) @pytest.fixture def db_path(tmp_path): """Fresh SQLite DB for each test.""" return tmp_path / "test_approvals.db" class TestGoldenTimmy: """Test the Golden Timmy constant.""" def test_golden_timmy_default_true(self): assert GOLDEN_TIMMY is True class TestApprovalItem: """Test ApprovalItem dataclass.""" def test_create(self): item = ApprovalItem( id="test-id", title="Deploy update", description="Deploy v2.0 to production", proposed_action="git push && deploy", impact="high", created_at=datetime.now(UTC), status="pending", ) assert item.id == "test-id" assert item.status == "pending" assert item.impact == "high" class TestCreateItem: """Test create_item persistence.""" def test_create_and_retrieve(self, db_path): item = create_item( title="Test approval", description="A test action", proposed_action="run tests", impact="low", db_path=db_path, ) assert item.id # UUID generated assert item.status == "pending" assert item.title == "Test approval" # Retrieve it retrieved = get_item(item.id, db_path) assert retrieved is not None assert retrieved.id == item.id assert retrieved.title == "Test approval" def test_create_default_impact(self, db_path): item = create_item( title="T", description="D", proposed_action="A", db_path=db_path, ) assert item.impact == "low" class TestListPending: """Test list_pending.""" def test_empty_db(self, db_path): items = list_pending(db_path) assert items == [] def test_only_pending(self, db_path): item1 = create_item("A", "D", "A", db_path=db_path) item2 = create_item("B", "D", "A", db_path=db_path) approve(item1.id, db_path) pending = list_pending(db_path) assert len(pending) == 1 assert pending[0].id == item2.id def test_ordered_newest_first(self, db_path): create_item("First", "D", "A", db_path=db_path) create_item("Second", "D", "A", db_path=db_path) pending = list_pending(db_path) assert pending[0].title == "Second" class TestListAll: """Test list_all.""" def test_includes_all_statuses(self, db_path): item1 = create_item("A", "D", "A", db_path=db_path) item2 = create_item("B", "D", "A", db_path=db_path) approve(item1.id, db_path) reject(item2.id, db_path) all_items = list_all(db_path) assert len(all_items) == 2 class TestApproveReject: """Test approve and reject operations.""" def test_approve_item(self, db_path): item = create_item("T", "D", "A", db_path=db_path) result = approve(item.id, db_path) assert result.status == "approved" def test_reject_item(self, db_path): item = create_item("T", "D", "A", db_path=db_path) result = reject(item.id, db_path) assert result.status == "rejected" def test_get_nonexistent_returns_none(self, db_path): result = get_item("nonexistent-id", db_path) assert result is None class TestExpireOld: """Test expire_old cleanup.""" def test_expire_removes_old_pending(self, db_path): # Create item and manually backdate it item = create_item("Old", "D", "A", db_path=db_path) old_date = (datetime.now(UTC) - timedelta(days=30)).isoformat() with _get_conn(db_path) as conn: conn.execute( "UPDATE approval_items SET created_at = ? WHERE id = ?", (old_date, item.id), ) conn.commit() count = expire_old(db_path) assert count == 1 # Should be gone assert get_item(item.id, db_path) is None def test_expire_keeps_recent(self, db_path): create_item("Recent", "D", "A", db_path=db_path) count = expire_old(db_path) assert count == 0 assert len(list_pending(db_path)) == 1 def test_expire_keeps_approved(self, db_path): item = create_item("Approved", "D", "A", db_path=db_path) approve(item.id, db_path) # Backdate it old_date = (datetime.now(UTC) - timedelta(days=30)).isoformat() with _get_conn(db_path) as conn: conn.execute( "UPDATE approval_items SET created_at = ? WHERE id = ?", (old_date, item.id), ) conn.commit() count = expire_old(db_path) assert count == 0 # approved items not expired