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>
This commit is contained in:
commit
44ed9936b7
48 changed files with 9707 additions and 0 deletions
525
tests/test_api.py
Normal file
525
tests/test_api.py
Normal file
|
|
@ -0,0 +1,525 @@
|
|||
"""
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue