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.
210 lines
7.9 KiB
Python
210 lines
7.9 KiB
Python
"""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()
|