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:
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"]
|
||||
Reference in New Issue
Block a user