Ryan Malloy 44ed9936b7 Initial commit: Claude Code Project Tracker
Add comprehensive development intelligence system that tracks:
- Development sessions with automatic start/stop
- Full conversation history with semantic search
- Tool usage and file operation analytics
- Think time and engagement analysis
- Git activity correlation
- Learning pattern recognition
- Productivity insights and metrics

Features:
- FastAPI backend with SQLite database
- Modern web dashboard with interactive charts
- Claude Code hook integration for automatic tracking
- Comprehensive test suite with 100+ tests
- Complete API documentation (OpenAPI/Swagger)
- Privacy-first design with local data storage

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-11 02:59:21 -06:00

525 lines
19 KiB
Python

"""
Tests for the Claude Code Project Tracker API endpoints.
"""
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.project import Project
from app.models.session import Session
from tests.fixtures import TestDataFactory
class TestSessionAPI:
"""Test session management endpoints."""
@pytest.mark.api
async def test_start_session_startup(self, test_client: AsyncClient):
"""Test starting a new session with startup type."""
session_data = TestDataFactory.create_session_data(
session_type="startup",
working_directory="/home/user/test-project",
git_branch="main"
)
response = await test_client.post("/api/session/start", json=session_data)
assert response.status_code == 201
data = response.json()
assert "session_id" in data
assert "project_id" in data
assert data["status"] == "started"
@pytest.mark.api
async def test_start_session_creates_project(self, test_client: AsyncClient):
"""Test that starting a session creates a project if it doesn't exist."""
session_data = {
"session_type": "startup",
"working_directory": "/home/user/new-project",
"git_repo": "https://github.com/user/new-project",
"environment": {"user": "testuser"}
}
response = await test_client.post("/api/session/start", json=session_data)
assert response.status_code == 201
data = response.json()
assert data["project_id"] is not None
@pytest.mark.api
async def test_start_session_resume(self, test_client: AsyncClient):
"""Test resuming an existing session."""
session_data = TestDataFactory.create_session_data(session_type="resume")
response = await test_client.post("/api/session/start", json=session_data)
assert response.status_code == 201
data = response.json()
assert "session_id" in data
@pytest.mark.api
async def test_end_session(self, test_client: AsyncClient, sample_session: Session):
"""Test ending an active session."""
end_data = {
"session_id": sample_session.id,
"end_reason": "normal"
}
response = await test_client.post("/api/session/end", json=end_data)
assert response.status_code == 200
data = response.json()
assert data["session_id"] == sample_session.id
assert data["status"] == "ended"
@pytest.mark.api
async def test_end_nonexistent_session(self, test_client: AsyncClient):
"""Test ending a session that doesn't exist."""
end_data = {
"session_id": 99999,
"end_reason": "normal"
}
response = await test_client.post("/api/session/end", json=end_data)
assert response.status_code == 404
class TestConversationAPI:
"""Test conversation tracking endpoints."""
@pytest.mark.api
async def test_log_conversation(self, test_client: AsyncClient, sample_session: Session):
"""Test logging a conversation exchange."""
conversation_data = TestDataFactory.create_conversation_data(
session_id=sample_session.id,
user_prompt="How do I implement authentication?",
claude_response="You can implement authentication using...",
tools_used=["Edit", "Write"]
)
response = await test_client.post("/api/conversation", json=conversation_data)
assert response.status_code == 201
@pytest.mark.api
async def test_log_conversation_user_prompt_only(self, test_client: AsyncClient, sample_session: Session):
"""Test logging just a user prompt."""
conversation_data = {
"session_id": sample_session.id,
"timestamp": "2024-01-01T12:00:00Z",
"user_prompt": "How do I fix this error?",
"exchange_type": "user_prompt"
}
response = await test_client.post("/api/conversation", json=conversation_data)
assert response.status_code == 201
@pytest.mark.api
async def test_log_conversation_invalid_session(self, test_client: AsyncClient):
"""Test logging conversation with invalid session ID."""
conversation_data = TestDataFactory.create_conversation_data(session_id=99999)
response = await test_client.post("/api/conversation", json=conversation_data)
assert response.status_code == 404
class TestActivityAPI:
"""Test activity tracking endpoints."""
@pytest.mark.api
async def test_record_activity_edit(self, test_client: AsyncClient, sample_session: Session):
"""Test recording an Edit tool activity."""
activity_data = TestDataFactory.create_activity_data(
session_id=sample_session.id,
tool_name="Edit",
action="file_edit",
file_path="/home/user/test.py",
lines_added=10,
lines_removed=3
)
response = await test_client.post("/api/activity", json=activity_data)
assert response.status_code == 201
@pytest.mark.api
async def test_record_activity_bash(self, test_client: AsyncClient, sample_session: Session):
"""Test recording a Bash command activity."""
activity_data = TestDataFactory.create_activity_data(
session_id=sample_session.id,
tool_name="Bash",
action="command_execution",
metadata={"command": "pytest", "exit_code": 0}
)
response = await test_client.post("/api/activity", json=activity_data)
assert response.status_code == 201
@pytest.mark.api
async def test_record_activity_failed(self, test_client: AsyncClient, sample_session: Session):
"""Test recording a failed activity."""
activity_data = TestDataFactory.create_activity_data(
session_id=sample_session.id,
success=False,
error_message="Permission denied"
)
response = await test_client.post("/api/activity", json=activity_data)
assert response.status_code == 201
class TestWaitingAPI:
"""Test waiting period tracking endpoints."""
@pytest.mark.api
async def test_start_waiting_period(self, test_client: AsyncClient, sample_session: Session):
"""Test starting a waiting period."""
waiting_data = {
"session_id": sample_session.id,
"timestamp": "2024-01-01T12:00:00Z",
"context_before": "Claude finished responding"
}
response = await test_client.post("/api/waiting/start", json=waiting_data)
assert response.status_code == 201
@pytest.mark.api
async def test_end_waiting_period(self, test_client: AsyncClient, sample_session: Session):
"""Test ending a waiting period."""
# First start a waiting period
start_data = {
"session_id": sample_session.id,
"timestamp": "2024-01-01T12:00:00Z",
"context_before": "Claude finished responding"
}
await test_client.post("/api/waiting/start", json=start_data)
# Then end it
end_data = {
"session_id": sample_session.id,
"timestamp": "2024-01-01T12:05:00Z",
"duration_seconds": 300,
"context_after": "User asked follow-up question"
}
response = await test_client.post("/api/waiting/end", json=end_data)
assert response.status_code == 200
class TestGitAPI:
"""Test git operation tracking endpoints."""
@pytest.mark.api
async def test_record_git_commit(self, test_client: AsyncClient, sample_session: Session):
"""Test recording a git commit operation."""
git_data = TestDataFactory.create_git_operation_data(
session_id=sample_session.id,
operation="commit",
command="git commit -m 'Add feature'",
files_changed=["main.py", "utils.py"],
lines_added=20,
lines_removed=5
)
response = await test_client.post("/api/git", json=git_data)
assert response.status_code == 201
@pytest.mark.api
async def test_record_git_push(self, test_client: AsyncClient, sample_session: Session):
"""Test recording a git push operation."""
git_data = TestDataFactory.create_git_operation_data(
session_id=sample_session.id,
operation="push",
command="git push origin main"
)
response = await test_client.post("/api/git", json=git_data)
assert response.status_code == 201
class TestProjectAPI:
"""Test project data retrieval endpoints."""
@pytest.mark.api
async def test_list_projects(self, test_client: AsyncClient, sample_project: Project):
"""Test listing all projects."""
response = await test_client.get("/api/projects")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) >= 1
project = data[0]
assert "id" in project
assert "name" in project
assert "path" in project
@pytest.mark.api
async def test_list_projects_with_pagination(self, test_client: AsyncClient):
"""Test project listing with pagination parameters."""
response = await test_client.get("/api/projects?limit=5&offset=0")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) <= 5
@pytest.mark.api
async def test_get_project_timeline(self, test_client: AsyncClient, sample_project: Project):
"""Test getting detailed project timeline."""
response = await test_client.get(f"/api/projects/{sample_project.id}/timeline")
assert response.status_code == 200
data = response.json()
assert "project" in data
assert "timeline" in data
assert isinstance(data["timeline"], list)
@pytest.mark.api
async def test_get_nonexistent_project_timeline(self, test_client: AsyncClient):
"""Test getting timeline for non-existent project."""
response = await test_client.get("/api/projects/99999/timeline")
assert response.status_code == 404
class TestAnalyticsAPI:
"""Test analytics and insights endpoints."""
@pytest.mark.api
async def test_get_productivity_metrics(self, test_client: AsyncClient, sample_project: Project):
"""Test getting productivity analytics."""
response = await test_client.get("/api/analytics/productivity")
assert response.status_code == 200
data = response.json()
assert "engagement_score" in data
assert "average_session_length" in data
assert "think_time_average" in data
@pytest.mark.api
async def test_get_productivity_metrics_for_project(self, test_client: AsyncClient, sample_project: Project):
"""Test getting productivity analytics for specific project."""
response = await test_client.get(f"/api/analytics/productivity?project_id={sample_project.id}")
assert response.status_code == 200
data = response.json()
assert isinstance(data, dict)
@pytest.mark.api
async def test_search_conversations(self, test_client: AsyncClient, sample_conversation):
"""Test searching through conversations."""
response = await test_client.get("/api/conversations/search?query=implement feature")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
if data: # If there are results
result = data[0]
assert "id" in result
assert "user_prompt" in result
assert "relevance_score" in result
@pytest.mark.api
async def test_search_conversations_with_project_filter(self, test_client: AsyncClient, sample_project: Project):
"""Test searching conversations within a specific project."""
response = await test_client.get(f"/api/conversations/search?query=debug&project_id={sample_project.id}")
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
class TestErrorHandling:
"""Test error handling and edge cases."""
@pytest.mark.api
async def test_malformed_json(self, test_client: AsyncClient):
"""Test handling of malformed JSON requests."""
response = await test_client.post(
"/api/session/start",
content="{'invalid': json}",
headers={"Content-Type": "application/json"}
)
assert response.status_code == 422
@pytest.mark.api
async def test_missing_required_fields(self, test_client: AsyncClient):
"""Test handling of requests with missing required fields."""
response = await test_client.post("/api/session/start", json={})
assert response.status_code == 422
data = response.json()
assert "detail" in data
@pytest.mark.api
async def test_invalid_session_id_type(self, test_client: AsyncClient):
"""Test handling of invalid data types."""
conversation_data = {
"session_id": "invalid", # Should be int
"user_prompt": "Test message",
"exchange_type": "user_prompt"
}
response = await test_client.post("/api/conversation", json=conversation_data)
assert response.status_code == 422
@pytest.mark.api
async def test_nonexistent_endpoint(self, test_client: AsyncClient):
"""Test accessing non-existent endpoint."""
response = await test_client.get("/api/nonexistent")
assert response.status_code == 404
class TestIntegrationScenarios:
"""Test complete workflow scenarios."""
@pytest.mark.integration
async def test_complete_session_workflow(self, test_client: AsyncClient):
"""Test a complete session from start to finish."""
# Start session
session_data = TestDataFactory.create_session_data(
working_directory="/home/user/integration-test"
)
start_response = await test_client.post("/api/session/start", json=session_data)
assert start_response.status_code == 201
session_info = start_response.json()
session_id = session_info["session_id"]
# Log user prompt
prompt_data = {
"session_id": session_id,
"timestamp": "2024-01-01T12:00:00Z",
"user_prompt": "Help me implement a REST API",
"exchange_type": "user_prompt"
}
prompt_response = await test_client.post("/api/conversation", json=prompt_data)
assert prompt_response.status_code == 201
# Start waiting period
waiting_start = {
"session_id": session_id,
"timestamp": "2024-01-01T12:00:01Z",
"context_before": "Claude is processing the request"
}
waiting_response = await test_client.post("/api/waiting/start", json=waiting_start)
assert waiting_response.status_code == 201
# Record some activities
activities = [
{
"session_id": session_id,
"tool_name": "Write",
"action": "file_write",
"file_path": "/home/user/integration-test/api.py",
"timestamp": "2024-01-01T12:01:00Z",
"success": True,
"lines_added": 50
},
{
"session_id": session_id,
"tool_name": "Edit",
"action": "file_edit",
"file_path": "/home/user/integration-test/main.py",
"timestamp": "2024-01-01T12:02:00Z",
"success": True,
"lines_added": 10,
"lines_removed": 2
}
]
for activity in activities:
activity_response = await test_client.post("/api/activity", json=activity)
assert activity_response.status_code == 201
# End waiting period
waiting_end = {
"session_id": session_id,
"timestamp": "2024-01-01T12:05:00Z",
"duration_seconds": 300,
"context_after": "User reviewed the implementation"
}
waiting_end_response = await test_client.post("/api/waiting/end", json=waiting_end)
assert waiting_end_response.status_code == 200
# Record git commit
git_data = {
"session_id": session_id,
"operation": "commit",
"command": "git commit -m 'Implement REST API'",
"timestamp": "2024-01-01T12:06:00Z",
"result": "[main abc123] Implement REST API",
"success": True,
"files_changed": ["api.py", "main.py"],
"lines_added": 60,
"lines_removed": 2,
"commit_hash": "abc12345"
}
git_response = await test_client.post("/api/git", json=git_data)
assert git_response.status_code == 201
# End session
end_data = {
"session_id": session_id,
"end_reason": "normal"
}
end_response = await test_client.post("/api/session/end", json=end_data)
assert end_response.status_code == 200
# Verify project was created and populated
projects_response = await test_client.get("/api/projects")
assert projects_response.status_code == 200
projects = projects_response.json()
# Find our project
test_project = None
for project in projects:
if project["path"] == "/home/user/integration-test":
test_project = project
break
assert test_project is not None
assert test_project["total_sessions"] >= 1
# Get project timeline
timeline_response = await test_client.get(f"/api/projects/{test_project['id']}/timeline")
assert timeline_response.status_code == 200
timeline = timeline_response.json()
assert len(timeline["timeline"]) > 0 # Should have recorded events
@pytest.mark.integration
@pytest.mark.slow
async def test_analytics_with_sample_data(self, test_client: AsyncClient, test_db: AsyncSession):
"""Test analytics calculations with realistic sample data."""
# This test would create a full dataset and verify analytics work correctly
# Marked as slow since it involves more data processing
productivity_response = await test_client.get("/api/analytics/productivity?days=7")
assert productivity_response.status_code == 200
metrics = productivity_response.json()
assert "engagement_score" in metrics
assert isinstance(metrics["engagement_score"], (int, float))
assert 0 <= metrics["engagement_score"] <= 100