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>
525 lines
19 KiB
Python
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 |