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:
102
tests/conftest.py
Normal file
102
tests/conftest.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Pytest fixtures for tracker tests."""
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from tracker.models import Project, Session, Note, NoteType, Change
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_project_dir(tmp_path):
|
||||
"""Create a temporary directory for project tests."""
|
||||
projects_root = tmp_path / "projects"
|
||||
projects_root.mkdir()
|
||||
return projects_root
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_project_data():
|
||||
"""Sample project data for testing."""
|
||||
return {
|
||||
"id": "test-project-id-123",
|
||||
"name": "Test Project",
|
||||
"slug": "test-project",
|
||||
"description": "A test project for unit testing",
|
||||
"type": "code",
|
||||
"status": "active",
|
||||
"tags": ["python", "testing"],
|
||||
"root_path": "/path/to/projects/test-project",
|
||||
"repo_path": "/path/to/repos/test-project",
|
||||
"created_at": datetime(2024, 1, 15, 10, 0, 0),
|
||||
"updated_at": datetime(2024, 1, 15, 10, 0, 0),
|
||||
"last_session_at": None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_project(sample_project_data):
|
||||
"""Create a sample Project instance."""
|
||||
return Project(**sample_project_data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session(sample_project):
|
||||
"""Create a mock session for testing."""
|
||||
return Session(
|
||||
id="session-123",
|
||||
project_slug=sample_project.slug,
|
||||
started_at=datetime(2024, 1, 15, 10, 0, 0),
|
||||
ended_at=datetime(2024, 1, 15, 11, 30, 0),
|
||||
duration_minutes=90,
|
||||
objective="Complete initial implementation",
|
||||
summary="Worked on core features",
|
||||
work_done=["Implemented feature A", "Fixed bug B"],
|
||||
changes=["Added new endpoint"],
|
||||
decisions=["Use JSON for storage"],
|
||||
blockers=[],
|
||||
next_steps=["Add tests", "Write documentation"],
|
||||
references=["https://example.com/docs"],
|
||||
raw_notes=[
|
||||
{"type": "work", "text": "Working on feature A", "timestamp": "2024-01-15T10:15:00"},
|
||||
{"type": "idea", "text": "Consider using caching", "timestamp": "2024-01-15T10:30:00"},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_note():
|
||||
"""Create a sample note for testing."""
|
||||
return Note(
|
||||
type=NoteType.WORK,
|
||||
text="Completed the implementation of feature X",
|
||||
created_at=datetime(2024, 1, 15, 10, 30, 0),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_change():
|
||||
"""Create a sample change for testing."""
|
||||
return Change(
|
||||
date=datetime(2024, 1, 15).date(),
|
||||
type="code",
|
||||
title="Added user authentication",
|
||||
impact="Improved security",
|
||||
references=["#123"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_project_with_structure(tmp_project_dir, sample_project_data):
|
||||
"""Create a temporary project with directory structure."""
|
||||
slug = sample_project_data["slug"]
|
||||
project_root = tmp_project_dir / slug
|
||||
(project_root / "sessions").mkdir(parents=True)
|
||||
(project_root / "docs").mkdir(parents=True)
|
||||
(project_root / "assets").mkdir(parents=True)
|
||||
(project_root / "meta").mkdir(parents=True)
|
||||
return project_root
|
||||
337
tests/test_flow.py
Normal file
337
tests/test_flow.py
Normal file
@@ -0,0 +1,337 @@
|
||||
"""Tests for complete flow: init -> start -> note -> stop -> show."""
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from tracker.models import Session, Note, NoteType
|
||||
from tracker.services import (
|
||||
create_project,
|
||||
get_project,
|
||||
ensure_project_structure,
|
||||
set_active_session,
|
||||
get_active_session,
|
||||
clear_active_session,
|
||||
validate_no_other_active_session,
|
||||
add_note,
|
||||
consolidate_notes,
|
||||
get_projects_root,
|
||||
)
|
||||
from tracker.storage import FileStorage
|
||||
|
||||
|
||||
class TestInitProjectFlow:
|
||||
"""Tests for project initialization flow."""
|
||||
|
||||
def test_init_project_creates_structure(self, tmp_path, sample_project_data):
|
||||
"""Test that project initialization creates required directory structure."""
|
||||
slug = sample_project_data["slug"]
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
project = create_project(
|
||||
name=sample_project_data["name"],
|
||||
slug=slug,
|
||||
description=sample_project_data["description"],
|
||||
type=sample_project_data["type"],
|
||||
)
|
||||
|
||||
ensure_project_structure(slug)
|
||||
|
||||
project_root = tmp_path / slug
|
||||
assert (project_root / "sessions").is_dir()
|
||||
assert (project_root / "docs").is_dir()
|
||||
assert (project_root / "assets").is_dir()
|
||||
assert (project_root / "meta").is_dir()
|
||||
|
||||
def test_init_project_creates_meta_file(self, tmp_path, sample_project_data):
|
||||
"""Test that project initialization creates meta/project.yaml."""
|
||||
slug = sample_project_data["slug"]
|
||||
storage = FileStorage(tmp_path)
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
project = create_project(
|
||||
name=sample_project_data["name"],
|
||||
slug=slug,
|
||||
description=sample_project_data["description"],
|
||||
type=sample_project_data["type"],
|
||||
)
|
||||
|
||||
# Write the project meta (simulating what storage would do)
|
||||
meta_data = {
|
||||
"id": project.id,
|
||||
"name": project.name,
|
||||
"slug": project.slug,
|
||||
"description": project.description,
|
||||
"type": project.type,
|
||||
"status": project.status,
|
||||
"tags": project.tags,
|
||||
"created_at": project.created_at.isoformat(),
|
||||
"updated_at": project.updated_at.isoformat(),
|
||||
}
|
||||
storage.write_project_meta(slug, meta_data)
|
||||
|
||||
meta_path = tmp_path / slug / "meta" / "project.yaml"
|
||||
assert meta_path.exists()
|
||||
|
||||
with open(meta_path, "r") as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
assert data["name"] == sample_project_data["name"]
|
||||
assert data["slug"] == slug
|
||||
|
||||
|
||||
class TestStartSessionFlow:
|
||||
"""Tests for starting a session."""
|
||||
|
||||
def test_start_session(self, tmp_path, sample_project_data, monkeypatch):
|
||||
"""Test starting a session creates active session."""
|
||||
slug = sample_project_data["slug"]
|
||||
|
||||
# Create a fake active session path
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
project = create_project(
|
||||
name=sample_project_data["name"],
|
||||
slug=slug,
|
||||
)
|
||||
ensure_project_structure(slug)
|
||||
|
||||
session = Session(
|
||||
id="session-1",
|
||||
project_slug=slug,
|
||||
started_at=datetime.now(),
|
||||
objective="Test objective",
|
||||
)
|
||||
|
||||
set_active_session(session)
|
||||
|
||||
active = get_active_session()
|
||||
assert active is not None
|
||||
assert active.id == "session-1"
|
||||
assert active.project_slug == slug
|
||||
|
||||
def test_start_session_fails_if_other_active(self, tmp_path, sample_project_data, monkeypatch):
|
||||
"""Test that starting a session fails if another project has an active session."""
|
||||
slug1 = "project-1"
|
||||
slug2 = "project-2"
|
||||
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
session1 = Session(
|
||||
id="session-1",
|
||||
project_slug=slug1,
|
||||
started_at=datetime.now(),
|
||||
)
|
||||
set_active_session(session1)
|
||||
|
||||
is_valid = True
|
||||
if not validate_no_other_active_session(slug2):
|
||||
is_valid = False
|
||||
|
||||
assert is_valid is False
|
||||
|
||||
|
||||
class TestAddNoteFlow:
|
||||
"""Tests for adding notes during a session."""
|
||||
|
||||
def test_add_note(self, tmp_path, sample_project_data, monkeypatch):
|
||||
"""Test adding a note during a session."""
|
||||
slug = sample_project_data["slug"]
|
||||
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
session = Session(
|
||||
id="session-1",
|
||||
project_slug=slug,
|
||||
started_at=datetime.now(),
|
||||
)
|
||||
set_active_session(session)
|
||||
|
||||
add_note(session, "work", "Test work note")
|
||||
|
||||
assert len(session.raw_notes) == 1
|
||||
assert session.raw_notes[0]["type"] == "work"
|
||||
assert session.raw_notes[0]["text"] == "Test work note"
|
||||
|
||||
def test_add_multiple_notes(self, tmp_path, sample_project_data, monkeypatch):
|
||||
"""Test adding multiple notes during a session."""
|
||||
slug = sample_project_data["slug"]
|
||||
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
session = Session(
|
||||
id="session-1",
|
||||
project_slug=slug,
|
||||
started_at=datetime.now(),
|
||||
)
|
||||
|
||||
add_note(session, "work", "Work note")
|
||||
add_note(session, "idea", "Idea note")
|
||||
add_note(session, "blocker", "Blocker note")
|
||||
|
||||
assert len(session.raw_notes) == 3
|
||||
assert session.raw_notes[0]["type"] == "work"
|
||||
assert session.raw_notes[1]["type"] == "idea"
|
||||
assert session.raw_notes[2]["type"] == "blocker"
|
||||
|
||||
|
||||
class TestStopSessionFlow:
|
||||
"""Tests for stopping a session."""
|
||||
|
||||
def test_stop_session(self, tmp_path, sample_project_data, monkeypatch):
|
||||
"""Test stopping a session clears active session."""
|
||||
slug = sample_project_data["slug"]
|
||||
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
session = Session(
|
||||
id="session-1",
|
||||
project_slug=slug,
|
||||
started_at=datetime.now(),
|
||||
)
|
||||
set_active_session(session)
|
||||
|
||||
session.ended_at = datetime.now()
|
||||
session.duration_minutes = 30
|
||||
|
||||
clear_active_session()
|
||||
|
||||
active = get_active_session()
|
||||
assert active is None
|
||||
|
||||
|
||||
class TestShowProjectFlow:
|
||||
"""Tests for showing project information."""
|
||||
|
||||
def test_show_project(self, tmp_path, sample_project_data):
|
||||
"""Test showing project information."""
|
||||
slug = sample_project_data["slug"]
|
||||
storage = FileStorage(tmp_path)
|
||||
|
||||
meta_data = {
|
||||
"id": "test-id",
|
||||
"name": sample_project_data["name"],
|
||||
"slug": slug,
|
||||
"description": sample_project_data["description"],
|
||||
"type": sample_project_data["type"],
|
||||
"status": sample_project_data["status"],
|
||||
"tags": sample_project_data["tags"],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}
|
||||
storage.write_project_meta(slug, meta_data)
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
project = get_project(slug)
|
||||
assert project is not None
|
||||
assert project.name == sample_project_data["name"]
|
||||
assert project.slug == slug
|
||||
|
||||
|
||||
class TestConsolidateNotes:
|
||||
"""Tests for consolidating notes after session."""
|
||||
|
||||
def test_consolidate_notes_categorizes_work(self):
|
||||
"""Test that consolidate_notes categorizes work notes."""
|
||||
raw_notes = [
|
||||
{"type": "work", "text": "Implemented feature A", "timestamp": "2024-01-15T10:00:00"},
|
||||
{"type": "work", "text": "Wrote tests", "timestamp": "2024-01-15T10:30:00"},
|
||||
]
|
||||
|
||||
consolidated = consolidate_notes(raw_notes)
|
||||
|
||||
assert len(consolidated["work_done"]) == 2
|
||||
assert "Implemented feature A" in consolidated["work_done"]
|
||||
assert "Wrote tests" in consolidated["work_done"]
|
||||
|
||||
def test_consolidate_notes_categorizes_blockers(self):
|
||||
"""Test that consolidate_notes categorizes blockers."""
|
||||
raw_notes = [
|
||||
{"type": "blocker", "text": "Waiting for API access", "timestamp": "2024-01-15T10:00:00"},
|
||||
]
|
||||
|
||||
consolidated = consolidate_notes(raw_notes)
|
||||
|
||||
assert len(consolidated["blockers"]) == 1
|
||||
assert "Waiting for API access" in consolidated["blockers"]
|
||||
|
||||
def test_consolidate_notes_categorizes_decisions(self):
|
||||
"""Test that consolidate_notes categorizes decisions."""
|
||||
raw_notes = [
|
||||
{"type": "decision", "text": "Use PostgreSQL for storage", "timestamp": "2024-01-15T10:00:00"},
|
||||
]
|
||||
|
||||
consolidated = consolidate_notes(raw_notes)
|
||||
|
||||
assert len(consolidated["decisions"]) == 1
|
||||
assert "Use PostgreSQL for storage" in consolidated["decisions"]
|
||||
|
||||
def test_consolidate_notes_categorizes_references(self):
|
||||
"""Test that consolidate_notes categorizes references."""
|
||||
raw_notes = [
|
||||
{"type": "reference", "text": "https://docs.example.com", "timestamp": "2024-01-15T10:00:00"},
|
||||
]
|
||||
|
||||
consolidated = consolidate_notes(raw_notes)
|
||||
|
||||
assert len(consolidated["references"]) == 1
|
||||
assert "https://docs.example.com" in consolidated["references"]
|
||||
|
||||
def test_consolidate_notes_categorizes_changes(self):
|
||||
"""Test that consolidate_notes categorizes changes."""
|
||||
raw_notes = [
|
||||
{"type": "change", "text": "Refactored authentication module", "timestamp": "2024-01-15T10:00:00"},
|
||||
]
|
||||
|
||||
consolidated = consolidate_notes(raw_notes)
|
||||
|
||||
assert len(consolidated["changes"]) == 1
|
||||
assert "Refactored authentication module" in consolidated["changes"]
|
||||
220
tests/test_models.py
Normal file
220
tests/test_models.py
Normal file
@@ -0,0 +1,220 @@
|
||||
"""Tests for data models."""
|
||||
|
||||
from datetime import datetime, date
|
||||
|
||||
import pytest
|
||||
|
||||
from tracker.models import Project, Session, Note, NoteType, Change
|
||||
|
||||
|
||||
class TestProjectCreation:
|
||||
"""Tests for Project model creation."""
|
||||
|
||||
def test_project_creation_with_all_fields(self, sample_project_data):
|
||||
"""Test creating a project with all fields specified."""
|
||||
project = Project(**sample_project_data)
|
||||
|
||||
assert project.id == sample_project_data["id"]
|
||||
assert project.name == sample_project_data["name"]
|
||||
assert project.slug == sample_project_data["slug"]
|
||||
assert project.description == sample_project_data["description"]
|
||||
assert project.type == sample_project_data["type"]
|
||||
assert project.status == sample_project_data["status"]
|
||||
assert project.tags == sample_project_data["tags"]
|
||||
assert project.root_path == sample_project_data["root_path"]
|
||||
assert project.repo_path == sample_project_data["repo_path"]
|
||||
assert project.created_at == sample_project_data["created_at"]
|
||||
assert project.updated_at == sample_project_data["updated_at"]
|
||||
assert project.last_session_at is None
|
||||
|
||||
def test_project_creation_with_defaults(self):
|
||||
"""Test creating a project with default values."""
|
||||
now = datetime.now()
|
||||
project = Project(
|
||||
id="test-id",
|
||||
name="Test",
|
||||
slug="test",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
assert project.description == ""
|
||||
assert project.type == "misc"
|
||||
assert project.status == "inbox"
|
||||
assert project.tags == []
|
||||
assert project.root_path == ""
|
||||
assert project.repo_path is None
|
||||
assert project.last_session_at is None
|
||||
|
||||
def test_project_status_validation(self):
|
||||
"""Test that project status can be set to valid values."""
|
||||
valid_statuses = ["inbox", "next", "active", "blocked", "waiting", "done", "archived"]
|
||||
|
||||
for status in valid_statuses:
|
||||
project = Project(
|
||||
id="test-id",
|
||||
name="Test",
|
||||
slug="test",
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
status=status,
|
||||
)
|
||||
assert project.status == status
|
||||
|
||||
def test_project_type_validation(self):
|
||||
"""Test that project type can be set to valid values."""
|
||||
valid_types = ["code", "homelab", "automation", "agent", "research", "misc"]
|
||||
|
||||
for project_type in valid_types:
|
||||
project = Project(
|
||||
id="test-id",
|
||||
name="Test",
|
||||
slug="test",
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
type=project_type,
|
||||
)
|
||||
assert project.type == project_type
|
||||
|
||||
|
||||
class TestSessionCreation:
|
||||
"""Tests for Session model creation."""
|
||||
|
||||
def test_session_creation_with_all_fields(self, mock_session):
|
||||
"""Test creating a session with all fields specified."""
|
||||
assert mock_session.id == "session-123"
|
||||
assert mock_session.project_slug == "test-project"
|
||||
assert mock_session.started_at == datetime(2024, 1, 15, 10, 0, 0)
|
||||
assert mock_session.ended_at == datetime(2024, 1, 15, 11, 30, 0)
|
||||
assert mock_session.duration_minutes == 90
|
||||
assert mock_session.objective == "Complete initial implementation"
|
||||
assert mock_session.summary == "Worked on core features"
|
||||
assert len(mock_session.work_done) == 2
|
||||
assert len(mock_session.changes) == 1
|
||||
assert len(mock_session.decisions) == 1
|
||||
assert len(mock_session.blockers) == 0
|
||||
assert len(mock_session.next_steps) == 2
|
||||
assert len(mock_session.references) == 1
|
||||
assert len(mock_session.raw_notes) == 2
|
||||
|
||||
def test_session_creation_with_defaults(self):
|
||||
"""Test creating a session with required fields only."""
|
||||
session = Session(
|
||||
id="session-1",
|
||||
project_slug="test-project",
|
||||
started_at=datetime.now(),
|
||||
)
|
||||
|
||||
assert session.ended_at is None
|
||||
assert session.duration_minutes is None
|
||||
assert session.objective == ""
|
||||
assert session.summary == ""
|
||||
assert session.work_done == []
|
||||
assert session.changes == []
|
||||
assert session.decisions == []
|
||||
assert session.blockers == []
|
||||
assert session.next_steps == []
|
||||
assert session.references == []
|
||||
assert session.raw_notes == []
|
||||
|
||||
def test_session_with_optional_datetime_fields(self):
|
||||
"""Test session with ended_at but no duration_minutes."""
|
||||
session = Session(
|
||||
id="session-1",
|
||||
project_slug="test-project",
|
||||
started_at=datetime(2024, 1, 15, 10, 0, 0),
|
||||
ended_at=datetime(2024, 1, 15, 11, 0, 0),
|
||||
)
|
||||
|
||||
assert session.ended_at is not None
|
||||
assert session.duration_minutes is None
|
||||
|
||||
|
||||
class TestNoteTypes:
|
||||
"""Tests for Note and NoteType models."""
|
||||
|
||||
def test_note_creation(self, sample_note):
|
||||
"""Test creating a note."""
|
||||
assert sample_note.type == NoteType.WORK
|
||||
assert sample_note.text == "Completed the implementation of feature X"
|
||||
assert sample_note.created_at == datetime(2024, 1, 15, 10, 30, 0)
|
||||
|
||||
def test_note_type_enum_values(self):
|
||||
"""Test that NoteType enum has expected values."""
|
||||
assert NoteType.WORK.value == "work"
|
||||
assert NoteType.CHANGE.value == "change"
|
||||
assert NoteType.BLOCKER.value == "blocker"
|
||||
assert NoteType.DECISION.value == "decision"
|
||||
assert NoteType.IDEA.value == "idea"
|
||||
assert NoteType.REFERENCE.value == "reference"
|
||||
|
||||
def test_note_type_assignment(self):
|
||||
"""Test assigning different note types to a note."""
|
||||
for note_type in NoteType:
|
||||
note = Note(type=note_type, text="Test note")
|
||||
assert note.type == note_type
|
||||
|
||||
def test_note_default_created_at(self):
|
||||
"""Test that note has default created_at timestamp."""
|
||||
note = Note(type=NoteType.WORK, text="Test note")
|
||||
assert note.created_at is not None
|
||||
assert isinstance(note.created_at, datetime)
|
||||
|
||||
def test_note_raw_notes_structure(self, mock_session):
|
||||
"""Test that raw_notes in session have expected structure."""
|
||||
assert len(mock_session.raw_notes) == 2
|
||||
assert mock_session.raw_notes[0]["type"] == "work"
|
||||
assert mock_session.raw_notes[0]["text"] == "Working on feature A"
|
||||
assert "timestamp" in mock_session.raw_notes[0]
|
||||
|
||||
|
||||
class TestChangeValidation:
|
||||
"""Tests for Change model validation."""
|
||||
|
||||
def test_change_creation(self, sample_change):
|
||||
"""Test creating a change."""
|
||||
assert sample_change.date == date(2024, 1, 15)
|
||||
assert sample_change.type == "code"
|
||||
assert sample_change.title == "Added user authentication"
|
||||
assert sample_change.impact == "Improved security"
|
||||
assert sample_change.references == ["#123"]
|
||||
|
||||
def test_change_creation_with_defaults(self):
|
||||
"""Test creating a change with default values."""
|
||||
change = Change(
|
||||
date=date(2024, 1, 15),
|
||||
type="docs",
|
||||
title="Updated documentation",
|
||||
)
|
||||
|
||||
assert change.impact == ""
|
||||
assert change.references == []
|
||||
|
||||
def test_change_date_as_date_not_datetime(self, sample_change):
|
||||
"""Test that change date is stored as date object."""
|
||||
assert isinstance(sample_change.date, date)
|
||||
assert not isinstance(sample_change.date, datetime)
|
||||
|
||||
def test_change_types(self):
|
||||
"""Test valid change types."""
|
||||
valid_types = ["code", "infra", "config", "docs", "automation", "decision"]
|
||||
|
||||
for change_type in valid_types:
|
||||
change = Change(
|
||||
date=date.today(),
|
||||
type=change_type,
|
||||
title=f"Test {change_type}",
|
||||
)
|
||||
assert change.type == change_type
|
||||
|
||||
def test_change_with_multiple_references(self):
|
||||
"""Test change with multiple references."""
|
||||
change = Change(
|
||||
date=date.today(),
|
||||
type="code",
|
||||
title="Major refactor",
|
||||
references=["#100", "#101", "#102"],
|
||||
)
|
||||
assert len(change.references) == 3
|
||||
assert "#100" in change.references
|
||||
assert "#102" in change.references
|
||||
209
tests/test_project_service.py
Normal file
209
tests/test_project_service.py
Normal 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()
|
||||
241
tests/test_session_service.py
Normal file
241
tests/test_session_service.py
Normal file
@@ -0,0 +1,241 @@
|
||||
"""Tests for SessionService."""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from tracker.models import Session
|
||||
from tracker.services import (
|
||||
get_active_session,
|
||||
set_active_session,
|
||||
clear_active_session,
|
||||
get_active_session_path,
|
||||
validate_no_other_active_session,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_active_session_path(tmp_path, monkeypatch):
|
||||
"""Mock the active session path to use tmp_path."""
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
return fake_path
|
||||
|
||||
|
||||
class TestSetAndGetActiveSession:
|
||||
"""Tests for set and get active session functions."""
|
||||
|
||||
def test_set_and_get_active_session(self, tmp_path, mock_session, monkeypatch):
|
||||
"""Test setting and getting an active session."""
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
set_active_session(mock_session)
|
||||
retrieved = get_active_session()
|
||||
|
||||
assert retrieved is not None
|
||||
assert retrieved.id == mock_session.id
|
||||
assert retrieved.project_slug == mock_session.project_slug
|
||||
assert retrieved.started_at == mock_session.started_at
|
||||
|
||||
def test_get_active_session_returns_none_when_no_session(self, tmp_path, monkeypatch):
|
||||
"""Test that get_active_session returns None when no session exists."""
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
result = get_active_session()
|
||||
assert result is None
|
||||
|
||||
def test_set_active_session_creates_parent_directories(self, tmp_path, mock_session, monkeypatch):
|
||||
"""Test that set_active_session creates parent directories if needed."""
|
||||
fake_path = tmp_path / "subdir" / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
set_active_session(mock_session)
|
||||
assert fake_path.exists()
|
||||
|
||||
def test_active_session_contains_all_fields(self, tmp_path, mock_session, monkeypatch):
|
||||
"""Test that active session file contains all session fields."""
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
set_active_session(mock_session)
|
||||
|
||||
with open(fake_path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert data["id"] == mock_session.id
|
||||
assert data["project_slug"] == mock_session.project_slug
|
||||
assert "started_at" in data
|
||||
assert "work_done" in data
|
||||
assert data["work_done"] == mock_session.work_done
|
||||
|
||||
|
||||
class TestClearActiveSession:
|
||||
"""Tests for clear_active_session function."""
|
||||
|
||||
def test_clear_active_session_removes_file(self, tmp_path, mock_session, monkeypatch):
|
||||
"""Test that clear_active_session removes the active session file."""
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
set_active_session(mock_session)
|
||||
assert fake_path.exists()
|
||||
|
||||
clear_active_session()
|
||||
assert not fake_path.exists()
|
||||
|
||||
def test_clear_active_session_when_no_session(self, tmp_path, monkeypatch):
|
||||
"""Test that clear_active_session does not error when no session exists."""
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
clear_active_session()
|
||||
assert not fake_path.exists()
|
||||
|
||||
def test_get_active_session_after_clear(self, tmp_path, mock_session, monkeypatch):
|
||||
"""Test that get_active_session returns None after clearing."""
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
set_active_session(mock_session)
|
||||
clear_active_session()
|
||||
|
||||
result = get_active_session()
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestValidateNoOtherActiveSession:
|
||||
"""Tests for validate_no_other_active_session function."""
|
||||
|
||||
def test_validate_no_other_active_session_returns_true_when_none(self, tmp_path, monkeypatch):
|
||||
"""Test validation returns True when no active session exists."""
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
result = validate_no_other_active_session("any-project")
|
||||
assert result is True
|
||||
|
||||
def test_validate_no_other_active_session_returns_true_for_same_project(
|
||||
self, tmp_path, mock_session, monkeypatch
|
||||
):
|
||||
"""Test validation returns True for same project's session."""
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
set_active_session(mock_session)
|
||||
|
||||
result = validate_no_other_active_session(mock_session.project_slug)
|
||||
assert result is True
|
||||
|
||||
def test_validate_no_other_active_session_returns_false_for_different_project(
|
||||
self, tmp_path, mock_session, monkeypatch
|
||||
):
|
||||
"""Test validation returns False for different project's session."""
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
set_active_session(mock_session)
|
||||
|
||||
result = validate_no_other_active_session("different-project")
|
||||
assert result is False
|
||||
|
||||
def test_validate_no_other_active_session_with_no_active_after_clear(
|
||||
self, tmp_path, mock_session, monkeypatch
|
||||
):
|
||||
"""Test validation returns True after clearing session."""
|
||||
fake_path = tmp_path / ".active_session.json"
|
||||
|
||||
def mock_get_active_session_path():
|
||||
return fake_path
|
||||
|
||||
monkeypatch.setattr(
|
||||
"tracker.services.session_service.get_active_session_path",
|
||||
mock_get_active_session_path
|
||||
)
|
||||
|
||||
set_active_session(mock_session)
|
||||
clear_active_session()
|
||||
|
||||
result = validate_no_other_active_session("any-project")
|
||||
assert result is True
|
||||
193
tests/test_storage.py
Normal file
193
tests/test_storage.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""Tests for FileStorage."""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from tracker.models import Session
|
||||
from tracker.storage import FileStorage
|
||||
|
||||
|
||||
class TestReadWriteProjectMeta:
|
||||
"""Tests for read/write project meta operations."""
|
||||
|
||||
def test_write_and_read_project_meta(self, tmp_project_dir):
|
||||
"""Test writing and reading project meta."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
slug = "test-project"
|
||||
|
||||
meta_data = {
|
||||
"id": "test-id",
|
||||
"name": "Test Project",
|
||||
"slug": slug,
|
||||
"description": "A test project",
|
||||
"type": "code",
|
||||
"status": "active",
|
||||
"tags": ["python"],
|
||||
"created_at": datetime(2024, 1, 15, 10, 0, 0).isoformat(),
|
||||
"updated_at": datetime(2024, 1, 15, 10, 0, 0).isoformat(),
|
||||
}
|
||||
|
||||
storage.write_project_meta(slug, meta_data)
|
||||
|
||||
result = storage.read_project_meta(slug)
|
||||
assert result["name"] == "Test Project"
|
||||
assert result["slug"] == slug
|
||||
assert result["type"] == "code"
|
||||
|
||||
def test_read_project_meta_creates_parent_directories(self, tmp_project_dir):
|
||||
"""Test that write_project_meta creates parent directories."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
slug = "new-project"
|
||||
|
||||
meta_data = {"id": "test-id", "name": "Test", "slug": slug}
|
||||
storage.write_project_meta(slug, meta_data)
|
||||
|
||||
meta_path = tmp_project_dir / slug / "meta" / "project.yaml"
|
||||
assert meta_path.exists()
|
||||
|
||||
def test_read_project_meta_raises_for_nonexistent(self, tmp_project_dir):
|
||||
"""Test that reading nonexistent project raises error."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
|
||||
with pytest.raises(FileNotFoundError):
|
||||
storage.read_project_meta("nonexistent")
|
||||
|
||||
|
||||
class TestAppendToLog:
|
||||
"""Tests for append_to_log operations."""
|
||||
|
||||
def test_append_to_log_creates_log_file(self, tmp_project_dir):
|
||||
"""Test that append_to_log creates LOG.md if it doesn't exist."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
slug = "test-project"
|
||||
|
||||
# Create project directory first
|
||||
(tmp_project_dir / slug).mkdir(parents=True)
|
||||
|
||||
storage.append_to_log(slug, "# Test Log Entry\n")
|
||||
|
||||
log_path = tmp_project_dir / slug / "LOG.md"
|
||||
assert log_path.exists()
|
||||
|
||||
def test_append_to_log_appends_content(self, tmp_project_dir):
|
||||
"""Test that append_to_log appends content to LOG.md."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
slug = "test-project"
|
||||
|
||||
# Create project directory first
|
||||
(tmp_project_dir / slug).mkdir(parents=True)
|
||||
|
||||
storage.append_to_log(slug, "# First Entry\n")
|
||||
storage.append_to_log(slug, "# Second Entry\n")
|
||||
|
||||
content = storage.read_log(slug)
|
||||
assert "# First Entry" in content
|
||||
assert "# Second Entry" in content
|
||||
|
||||
def test_read_log_returns_empty_string_for_nonexistent(self, tmp_project_dir):
|
||||
"""Test that read_log returns empty string for nonexistent log."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
|
||||
result = storage.read_log("nonexistent")
|
||||
assert result == ""
|
||||
|
||||
|
||||
class TestActiveSessionStorage:
|
||||
"""Tests for active session storage operations."""
|
||||
|
||||
def test_write_and_read_active_session(self, tmp_project_dir, mock_session):
|
||||
"""Test writing and reading active session."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
|
||||
session_data = mock_session.model_dump(mode="json")
|
||||
session_data["started_at"] = mock_session.started_at.isoformat()
|
||||
if mock_session.ended_at:
|
||||
session_data["ended_at"] = mock_session.ended_at.isoformat()
|
||||
|
||||
storage.write_active_session(session_data)
|
||||
|
||||
result = storage.read_active_session()
|
||||
assert result is not None
|
||||
assert result["id"] == mock_session.id
|
||||
|
||||
def test_read_active_session_returns_none_when_not_exists(self, tmp_project_dir):
|
||||
"""Test that read_active_session returns None when file doesn't exist."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
|
||||
result = storage.read_active_session()
|
||||
assert result is None
|
||||
|
||||
def test_delete_active_session(self, tmp_project_dir, mock_session):
|
||||
"""Test deleting active session."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
|
||||
session_data = mock_session.model_dump(mode="json")
|
||||
session_data["started_at"] = mock_session.started_at.isoformat()
|
||||
storage.write_active_session(session_data)
|
||||
|
||||
storage.delete_active_session()
|
||||
|
||||
result = storage.read_active_session()
|
||||
assert result is None
|
||||
|
||||
def test_delete_active_session_when_not_exists(self, tmp_project_dir):
|
||||
"""Test deleting active session when it doesn't exist doesn't error."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
|
||||
storage.delete_active_session()
|
||||
|
||||
|
||||
class TestProjectExistence:
|
||||
"""Tests for project existence checks."""
|
||||
|
||||
def test_project_exists_returns_true_for_existing_project(self, tmp_project_dir):
|
||||
"""Test that project_exists returns True for existing project."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
slug = "test-project"
|
||||
|
||||
(tmp_project_dir / slug).mkdir()
|
||||
|
||||
assert storage.project_exists(slug) is True
|
||||
|
||||
def test_project_exists_returns_false_for_nonexistent(self, tmp_project_dir):
|
||||
"""Test that project_exists returns False for nonexistent project."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
|
||||
assert storage.project_exists("nonexistent") is False
|
||||
|
||||
|
||||
class TestListProjects:
|
||||
"""Tests for listing projects."""
|
||||
|
||||
def test_list_projects_returns_all_projects(self, tmp_project_dir):
|
||||
"""Test that list_projects returns all project slugs."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
|
||||
(tmp_project_dir / "project-1").mkdir()
|
||||
(tmp_project_dir / "project-2").mkdir()
|
||||
|
||||
projects = storage.list_projects()
|
||||
assert "project-1" in projects
|
||||
assert "project-2" in projects
|
||||
|
||||
def test_list_projects_excludes_hidden_directories(self, tmp_project_dir):
|
||||
"""Test that list_projects excludes hidden directories."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
|
||||
(tmp_project_dir / "project-1").mkdir()
|
||||
(tmp_project_dir / ".hidden").mkdir()
|
||||
|
||||
projects = storage.list_projects()
|
||||
assert "project-1" in projects
|
||||
assert ".hidden" not in projects
|
||||
|
||||
def test_list_projects_returns_empty_list_when_no_projects(self, tmp_project_dir):
|
||||
"""Test that list_projects returns empty list when no projects exist."""
|
||||
storage = FileStorage(tmp_project_dir)
|
||||
|
||||
projects = storage.list_projects()
|
||||
assert projects == []
|
||||
Reference in New Issue
Block a user