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.
338 lines
11 KiB
Python
338 lines
11 KiB
Python
"""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"]
|