Add comprehensive test suite for MVP-1 Personal Tracker CLI

Implements 72 tests covering:
- Model tests (Project, Session, Note, Change)
- ProjectService tests (create, get, list, ensure structure)
- SessionService tests (active session management)
- FileStorage tests (read/write operations)
- Complete flow tests (init -> start -> note -> stop -> show)
- Note consolidation tests

Uses pytest with tmp_path fixtures for isolated testing.
This commit is contained in:
2026-03-23 09:40:46 -03:00
parent 4e67062c99
commit 2735562b65
6 changed files with 1302 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
"""Tests for ProjectService."""
import tempfile
from datetime import datetime
from pathlib import Path
from unittest.mock import patch
import pytest
import yaml
from tracker.models import Project
from tracker.services import (
create_project,
get_project,
update_project,
list_projects,
get_projects_root,
ensure_project_structure,
)
class TestCreateProject:
"""Tests for create_project function."""
def test_create_project_returns_project_instance(self):
"""Test that create_project returns a Project instance."""
project = create_project(
name="Test Project",
slug="test-project",
description="A test project",
type="code",
)
assert isinstance(project, Project)
assert project.name == "Test Project"
assert project.slug == "test-project"
assert project.description == "A test project"
assert project.type == "code"
def test_create_project_generates_id(self):
"""Test that create_project generates a UUID."""
project = create_project(name="Test", slug="test")
assert project.id is not None
assert len(project.id) > 0
def test_create_project_sets_default_status(self):
"""Test that create_project sets default status to inbox."""
project = create_project(name="Test", slug="test")
assert project.status == "inbox"
def test_create_project_sets_timestamps(self):
"""Test that create_project sets created_at and updated_at."""
before = datetime.now()
project = create_project(name="Test", slug="test")
after = datetime.now()
assert before <= project.created_at <= after
assert before <= project.updated_at <= after
# created_at and updated_at should be very close (within 1 second)
time_diff = abs((project.updated_at - project.created_at).total_seconds())
assert time_diff < 1
def test_create_project_with_tags(self):
"""Test creating a project with tags."""
tags = ["python", "testing", "cli"]
project = create_project(name="Test", slug="test", tags=tags)
assert project.tags == tags
def test_create_project_with_repo_path(self):
"""Test creating a project with a repo path."""
repo_path = Path("/path/to/repo")
project = create_project(name="Test", slug="test", repo_path=repo_path)
assert project.repo_path == str(repo_path)
def test_create_project_without_repo_path(self):
"""Test creating a project without repo path."""
project = create_project(name="Test", slug="test")
assert project.repo_path is None
class TestGetProject:
"""Tests for get_project function."""
def test_get_project_returns_none_for_nonexistent(self, tmp_path):
"""Test that get_project returns None for nonexistent project."""
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
result = get_project("nonexistent")
assert result is None
def test_get_project_returns_project_when_exists(self, tmp_path, sample_project_data):
"""Test that get_project returns Project when it exists."""
slug = sample_project_data["slug"]
project_dir = tmp_path / slug / "meta"
project_dir.mkdir(parents=True)
meta_path = project_dir / "project.yaml"
with open(meta_path, "w") as f:
yaml.safe_dump(sample_project_data, f)
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
project = get_project(slug)
assert project is not None
assert project.slug == slug
assert project.name == sample_project_data["name"]
def test_get_project_handles_invalid_yaml(self, tmp_path):
"""Test that get_project handles invalid YAML gracefully."""
slug = "test-project"
project_dir = tmp_path / slug / "meta"
project_dir.mkdir(parents=True)
meta_path = project_dir / "project.yaml"
with open(meta_path, "w") as f:
f.write("invalid: yaml: content:")
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
result = get_project(slug)
assert result is None
class TestListProjects:
"""Tests for list_projects function."""
def test_list_projects_returns_empty_list_when_no_projects(self, tmp_path):
"""Test that list_projects returns empty list when no projects exist."""
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
projects = list_projects()
assert projects == []
def test_list_projects_returns_all_projects(self, tmp_path, sample_project_data):
"""Test that list_projects returns all existing projects."""
project1_data = sample_project_data.copy()
project1_data["slug"] = "project-1"
project1_data["name"] = "Project 1"
project2_data = sample_project_data.copy()
project2_data["slug"] = "project-2"
project2_data["name"] = "Project 2"
for project_data in [project1_data, project2_data]:
slug = project_data["slug"]
project_dir = tmp_path / slug / "meta"
project_dir.mkdir(parents=True)
meta_path = project_dir / "project.yaml"
with open(meta_path, "w") as f:
yaml.safe_dump(project_data, f)
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
projects = list_projects()
assert len(projects) == 2
slugs = [p.slug for p in projects]
assert "project-1" in slugs
assert "project-2" in slugs
def test_list_projects_ignores_hidden_directories(self, tmp_path, sample_project_data):
"""Test that list_projects ignores hidden directories."""
slug = sample_project_data["slug"]
project_dir = tmp_path / slug / "meta"
project_dir.mkdir(parents=True)
meta_path = project_dir / "project.yaml"
with open(meta_path, "w") as f:
yaml.safe_dump(sample_project_data, f)
hidden_dir = tmp_path / ".hidden"
hidden_dir.mkdir()
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
projects = list_projects()
assert len(projects) == 1
assert projects[0].slug == slug
class TestEnsureProjectStructure:
"""Tests for ensure_project_structure function."""
def test_ensure_project_structure_creates_directories(self, tmp_path):
"""Test that ensure_project_structure creates required directories."""
slug = "test-project"
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
ensure_project_structure(slug)
project_root = tmp_path / slug
assert (project_root / "sessions").exists()
assert (project_root / "docs").exists()
assert (project_root / "assets").exists()
assert (project_root / "meta").exists()
def test_ensure_project_structure_is_idempotent(self, tmp_path):
"""Test that ensure_project_structure can be called multiple times safely."""
slug = "test-project"
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
ensure_project_structure(slug)
ensure_project_structure(slug)
project_root = tmp_path / slug
assert (project_root / "sessions").exists()
assert (project_root / "docs").exists()
def test_ensure_project_structure_creates_nested_directories(self, tmp_path):
"""Test that ensure_project_structure creates parent directories if needed."""
slug = "nested/project"
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
ensure_project_structure(slug)
project_root = tmp_path / slug
assert (project_root / "sessions").exists()