""" 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