import pytest async def setup_project_and_get_token(client): await client.post("/api/v1/auth/register", json={"username": "docuser", "password": "pass123"}) login = await client.post("/api/v1/auth/login", json={"username": "docuser", "password": "pass123"}) token = login.json()["access_token"] proj_resp = await client.post( "/api/v1/projects", json={"name": "Doc Test Project"}, headers={"Authorization": f"Bearer {token}"} ) return token, proj_resp.json()["id"] @pytest.mark.asyncio async def test_create_document(client): token, proj_id = await setup_project_and_get_token(client) response = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "My Document", "content": "# Hello\n\nWorld"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 201 data = response.json() assert data["title"] == "My Document" assert data["content"] == "# Hello\n\nWorld" assert data["project_id"] == proj_id @pytest.mark.asyncio async def test_get_document(client): token, proj_id = await setup_project_and_get_token(client) create_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Get Doc Test"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = create_resp.json()["id"] response = await client.get( f"/api/v1/documents/{doc_id}", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 assert response.json()["title"] == "Get Doc Test" @pytest.mark.asyncio async def test_update_document_content(client): token, proj_id = await setup_project_and_get_token(client) create_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Original Title", "content": "Original Content"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = create_resp.json()["id"] response = await client.put( f"/api/v1/documents/{doc_id}/content", json={"content": "Updated Content"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 assert response.json()["content"] == "Updated Content" @pytest.mark.asyncio async def test_soft_delete_document(client): token, proj_id = await setup_project_and_get_token(client) create_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "To Delete"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = create_resp.json()["id"] del_resp = await client.delete( f"/api/v1/documents/{doc_id}", headers={"Authorization": f"Bearer {token}"} ) assert del_resp.status_code == 204 @pytest.mark.asyncio async def test_assign_tag(client): token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Tagged Doc"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] tag_resp = await client.post( "/api/v1/tags", json={"name": "important", "color": "#ff0000"}, headers={"Authorization": f"Bearer {token}"} ) tag_id = tag_resp.json()["id"] assign_resp = await client.post( f"/api/v1/documents/{doc_id}/tags", json={"tag_ids": [tag_id]}, headers={"Authorization": f"Bearer {token}"} ) assert assign_resp.status_code == 204 get_resp = await client.get( f"/api/v1/documents/{doc_id}", headers={"Authorization": f"Bearer {token}"} ) assert len(get_resp.json()["tags"]) == 1 assert get_resp.json()["tags"][0]["name"] == "important" @pytest.mark.asyncio async def test_remove_tag(client): token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Tagged Doc 2"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] tag_resp = await client.post( "/api/v1/tags", json={"name": "temp", "color": "#00ff00"}, headers={"Authorization": f"Bearer {token}"} ) tag_id = tag_resp.json()["id"] await client.post( f"/api/v1/documents/{doc_id}/tags", json={"tag_ids": [tag_id]}, headers={"Authorization": f"Bearer {token}"} ) remove_resp = await client.delete( f"/api/v1/documents/{doc_id}/tags/{tag_id}", headers={"Authorization": f"Bearer {token}"} ) assert remove_resp.status_code == 204 # ============================================================================= # Phase 2: Reasoning Endpoint Tests # ============================================================================= @pytest.mark.asyncio async def test_get_document_reasoning_empty(client): """Test getting reasoning from a document with no reasoning set.""" token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "No Reasoning Doc"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] response = await client.get( f"/api/v1/documents/{doc_id}/reasoning", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["reasoning_type"] is None assert data["confidence"] is None assert data["reasoning_steps"] == [] assert data["model_source"] is None @pytest.mark.asyncio async def test_patch_document_reasoning(client): """Test updating reasoning metadata.""" token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Reasoning Doc"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] # Update reasoning response = await client.patch( f"/api/v1/documents/{doc_id}/reasoning", json={ "reasoning_type": "chain", "confidence": 0.87, "model_source": "claude-3", "reasoning_steps": [ {"step": 1, "thought": "First thought", "conclusion": "First conclusion"} ] }, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["reasoning_type"] == "chain" assert data["confidence"] == 0.87 assert data["model_source"] == "claude-3" assert len(data["reasoning_steps"]) == 1 @pytest.mark.asyncio async def test_patch_reasoning_invalid_confidence(client): """Test that invalid confidence values are rejected.""" token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Reasoning Doc 2"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] # Invalid confidence > 1.0 - Pydantic validation returns 422 response = await client.patch( f"/api/v1/documents/{doc_id}/reasoning", json={"confidence": 1.5}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 422 # Invalid confidence < 0.0 - Pydantic validation returns 422 response = await client.patch( f"/api/v1/documents/{doc_id}/reasoning", json={"confidence": -0.1}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 422 @pytest.mark.asyncio async def test_get_reasoning_panel(client): """Test getting reasoning panel data.""" token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Panel Doc"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] # Initially no reasoning panel_resp = await client.get( f"/api/v1/documents/{doc_id}/reasoning-panel", headers={"Authorization": f"Bearer {token}"} ) assert panel_resp.status_code == 200 panel = panel_resp.json() assert panel["has_reasoning"] is False assert panel["reasoning"] is None assert panel["editable"] is True # Add reasoning await client.patch( f"/api/v1/documents/{doc_id}/reasoning", json={"reasoning_type": "idea", "confidence": 0.95}, headers={"Authorization": f"Bearer {token}"} ) # Now has reasoning panel_resp = await client.get( f"/api/v1/documents/{doc_id}/reasoning-panel", headers={"Authorization": f"Bearer {token}"} ) panel = panel_resp.json() assert panel["has_reasoning"] is True assert panel["reasoning"]["reasoning_type"] == "idea" @pytest.mark.asyncio async def test_add_reasoning_step(client): """Test adding a reasoning step.""" token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Steps Doc"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] # Add first step step1_resp = await client.post( f"/api/v1/documents/{doc_id}/reasoning-steps", json={"thought": "First step thought", "conclusion": "First conclusion"}, headers={"Authorization": f"Bearer {token}"} ) assert step1_resp.status_code == 201 step1 = step1_resp.json() assert step1["step"] == 1 assert step1["thought"] == "First step thought" # Add second step step2_resp = await client.post( f"/api/v1/documents/{doc_id}/reasoning-steps", json={"thought": "Second step thought"}, headers={"Authorization": f"Bearer {token}"} ) assert step2_resp.status_code == 201 step2 = step2_resp.json() assert step2["step"] == 2 @pytest.mark.asyncio async def test_delete_reasoning_step(client): """Test deleting a reasoning step.""" token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Delete Step Doc"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] # Add steps await client.post( f"/api/v1/documents/{doc_id}/reasoning-steps", json={"thought": "Step 1"}, headers={"Authorization": f"Bearer {token}"} ) await client.post( f"/api/v1/documents/{doc_id}/reasoning-steps", json={"thought": "Step 2"}, headers={"Authorization": f"Bearer {token}"} ) # Delete step 1 del_resp = await client.delete( f"/api/v1/documents/{doc_id}/reasoning-steps/1", headers={"Authorization": f"Bearer {token}"} ) assert del_resp.status_code == 204 # Verify step 2 still exists reasoning_resp = await client.get( f"/api/v1/documents/{doc_id}/reasoning", headers={"Authorization": f"Bearer {token}"} ) steps = reasoning_resp.json()["reasoning_steps"] assert len(steps) == 1 assert steps[0]["step"] == 2 @pytest.mark.asyncio async def test_delete_nonexistent_step(client): """Test deleting a step that doesn't exist.""" token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "No Steps Doc"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] del_resp = await client.delete( f"/api/v1/documents/{doc_id}/reasoning-steps/999", headers={"Authorization": f"Bearer {token}"} ) assert del_resp.status_code == 404 # ============================================================================= # Phase 2: TipTap Content Endpoint Tests # ============================================================================= @pytest.mark.asyncio async def test_get_content_tiptap_format(client): """Test getting content in TipTap format.""" token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "TipTap Doc", "content": "Hello world"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] response = await client.get( f"/api/v1/documents/{doc_id}/content", params={"format": "tiptap"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["format"] == "tiptap" assert "content" in data @pytest.mark.asyncio async def test_get_content_markdown_format(client): """Test getting content in Markdown format.""" token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Markdown Doc", "content": "# Hello\n\nWorld"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] response = await client.get( f"/api/v1/documents/{doc_id}/content", params={"format": "markdown"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["format"] == "markdown" assert "# Hello" in data["content"] @pytest.mark.asyncio async def test_put_content_tiptap(client): """Test updating content with TipTap JSON.""" token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Update TipTap Doc"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] tiptap_content = { "type": "doc", "content": [ { "type": "heading", "attrs": {"level": 1}, "content": [{"type": "text", "text": "My Title"}] }, { "type": "paragraph", "content": [{"type": "text", "text": "Hello world"}] } ] } response = await client.put( f"/api/v1/documents/{doc_id}/content", json={"content": tiptap_content, "format": "tiptap"}, headers={"Authorization": f"Bearer {token}"} ) print(f"DEBUG: status={response.status_code} body={response.text}") assert response.status_code == 200, f"Expected 200, got {response.status_code}: {response.text}" data = response.json() assert data["tiptap_content"] is not None # Verify markdown content was also updated assert "My Title" in data["content"] or "# My Title" in data["content"] @pytest.mark.asyncio async def test_put_content_markdown(client): """Test updating content with Markdown.""" token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Update MD Doc"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] response = await client.put( f"/api/v1/documents/{doc_id}/content", json={"content": "# New Title\n\nNew content here", "format": "markdown"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["content"] == "# New Title\n\nNew content here" @pytest.mark.asyncio async def test_document_response_includes_reasoning_fields(client): """Test that DocumentResponse includes new reasoning fields.""" token, proj_id = await setup_project_and_get_token(client) doc_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Full Doc"}, headers={"Authorization": f"Bearer {token}"} ) doc_id = doc_resp.json()["id"] # Add reasoning await client.patch( f"/api/v1/documents/{doc_id}/reasoning", json={"reasoning_type": "reflection", "confidence": 0.99}, headers={"Authorization": f"Bearer {token}"} ) # Get document and verify fields get_resp = await client.get( f"/api/v1/documents/{doc_id}", headers={"Authorization": f"Bearer {token}"} ) data = get_resp.json() assert "reasoning_type" in data assert "confidence" in data assert "reasoning_steps" in data assert "model_source" in data assert "tiptap_content" in data assert data["reasoning_type"] == "reflection" assert data["confidence"] == 0.99