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

337
tests/test_flow.py Normal file
View 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"]