import pytest import uuid async def setup_project_documents(client): """Create agent, project, and 3 documents for link testing.""" await client.post("/api/v1/auth/register", json={"username": "linkuser", "password": "pass123"}) login = await client.post("/api/v1/auth/login", json={"username": "linkuser", "password": "pass123"}) token = login.json()["access_token"] proj_resp = await client.post( "/api/v1/projects", json={"name": "Link Test Project"}, headers={"Authorization": f"Bearer {token}"} ) proj_id = proj_resp.json()["id"] # Create doc1 doc1_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Document One", "content": "This is the first document"}, headers={"Authorization": f"Bearer {token}"} ) doc1_id = doc1_resp.json()["id"] # Create doc2 doc2_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Document Two", "content": "This is the second document"}, headers={"Authorization": f"Bearer {token}"} ) doc2_id = doc2_resp.json()["id"] # Create doc3 doc3_resp = await client.post( f"/api/v1/projects/{proj_id}/documents", json={"title": "Document Three", "content": "This is the third document"}, headers={"Authorization": f"Bearer {token}"} ) doc3_id = doc3_resp.json()["id"] return token, proj_id, doc1_id, doc2_id, doc3_id # ============================================================================= # Link Detection Tests # ============================================================================= @pytest.mark.asyncio async def test_detect_links_valid(client): """Test detecting valid [[uuid]] links in content.""" token, proj_id, doc1_id, doc2_id, doc3_id = await setup_project_documents(client) content = f"This references [[{doc2_id}]] and also [[{doc3_id}|Document Three]]" response = await client.post( f"/api/v1/documents/{doc1_id}/detect-links", json={"content": content}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["document_id"] == doc1_id assert set(data["outgoing_links"]) == {doc2_id, doc3_id} assert data["links_detected"] == 2 assert data["links_broken"] == 0 @pytest.mark.asyncio async def test_detect_links_broken(client): """Test detecting broken links (non-existent documents).""" token, proj_id, doc1_id, _, _ = await setup_project_documents(client) fake_id = str(uuid.uuid4()) content = f"This references [[{fake_id}]] which doesn't exist" response = await client.post( f"/api/v1/documents/{doc1_id}/detect-links", json={"content": content}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["outgoing_links"] == [] assert data["links_broken"] == 1 assert data["broken_links"][0]["reason"] == "document_not_found" @pytest.mark.asyncio async def test_detect_links_empty_content(client): """Test detect-links with no [[uuid]] patterns.""" token, proj_id, doc1_id, _, _ = await setup_project_documents(client) response = await client.post( f"/api/v1/documents/{doc1_id}/detect-links", json={"content": "No links here just plain text"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["outgoing_links"] == [] assert data["links_detected"] == 0 @pytest.mark.asyncio async def test_detect_links_preserves_order_and_dedups(client): """Test that outgoing_links preserves order and deduplicates.""" token, proj_id, doc1_id, doc2_id, doc3_id = await setup_project_documents(client) # Reference doc2 twice - should only appear once in output content = f"See [[{doc2_id}]] and again [[{doc2_id}]] and [[{doc3_id}]]" response = await client.post( f"/api/v1/documents/{doc1_id}/detect-links", json={"content": content}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() # Order should be preserved, duplicates removed assert data["outgoing_links"] == [doc2_id, doc3_id] @pytest.mark.asyncio async def test_detect_links_updates_backlinks_count(client): """Test that detect-links updates backlinks_count on target documents.""" token, proj_id, doc1_id, doc2_id, doc3_id = await setup_project_documents(client) # Add links from doc1 -> doc2 content = f"Links to [[{doc2_id}]] and [[{doc3_id}]]" await client.post( f"/api/v1/documents/{doc1_id}/detect-links", json={"content": content}, headers={"Authorization": f"Bearer {token}"} ) # Check backlinks count on doc2 doc2_get = await client.get( f"/api/v1/documents/{doc2_id}", headers={"Authorization": f"Bearer {token}"} ) # Note: the model may not expose backlinks_count directly in response # The count is tracked in DB for graph queries # ============================================================================= # Backlinks Tests # ============================================================================= @pytest.mark.asyncio async def test_get_outgoing_links(client): """Test getting outgoing links from a document.""" token, proj_id, doc1_id, doc2_id, doc3_id = await setup_project_documents(client) # Add links from doc1 -> doc2, doc3 content = f"References [[{doc2_id}]] and [[{doc3_id}]]" await client.post( f"/api/v1/documents/{doc1_id}/detect-links", json={"content": content}, headers={"Authorization": f"Bearer {token}"} ) response = await client.get( f"/api/v1/documents/{doc1_id}/outgoing-links", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["document_id"] == doc1_id assert data["outgoing_links_count"] == 2 link_ids = [l["document_id"] for l in data["outgoing_links"]] assert set(link_ids) == {doc2_id, doc3_id} for link in data["outgoing_links"]: assert link["exists"] is True @pytest.mark.asyncio async def test_get_outgoing_links_deleted_target(client): """Test outgoing links shows exists:false for deleted targets.""" token, proj_id, doc1_id, doc2_id, _ = await setup_project_documents(client) # Add link await client.post( f"/api/v1/documents/{doc1_id}/detect-links", json={"content": f"Links to [[{doc2_id}]]"}, headers={"Authorization": f"Bearer {token}"} ) # Delete doc2 await client.delete( f"/api/v1/documents/{doc2_id}", headers={"Authorization": f"Bearer {token}"} ) response = await client.get( f"/api/v1/documents/{doc1_id}/outgoing-links", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() # The outgoing_links should still show doc2_id but with exists:false # OR it might be filtered out depending on implementation # This test documents expected behavior @pytest.mark.asyncio async def test_get_backlinks(client): """Test getting backlinks to a document.""" token, proj_id, doc1_id, doc2_id, doc3_id = await setup_project_documents(client) # doc1 and doc2 both link to doc3 await client.post( f"/api/v1/documents/{doc1_id}/detect-links", json={"content": f"See [[{doc3_id}]] for details"}, headers={"Authorization": f"Bearer {token}"} ) await client.post( f"/api/v1/documents/{doc2_id}/detect-links", json={"content": f"Also see [[{doc3_id}]] here"}, headers={"Authorization": f"Bearer {token}"} ) response = await client.get( f"/api/v1/documents/{doc3_id}/backlinks", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["document_id"] == doc3_id assert data["backlinks_count"] == 2 backlink_ids = [b["document_id"] for b in data["backlinks"]] assert set(backlink_ids) == {doc1_id, doc2_id} @pytest.mark.asyncio async def test_get_backlinks_empty(client): """Test getting backlinks when no documents reference this one.""" token, proj_id, doc1_id, _, _ = await setup_project_documents(client) response = await client.get( f"/api/v1/documents/{doc1_id}/backlinks", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["backlinks"] == [] @pytest.mark.asyncio async def test_get_links_combined(client): """Test the combined /links endpoint returns both incoming and outgoing.""" token, proj_id, doc1_id, doc2_id, doc3_id = await setup_project_documents(client) # doc1 -> doc2 and doc3 # doc2 -> doc3 await client.post( f"/api/v1/documents/{doc1_id}/detect-links", json={"content": f"Links to [[{doc2_id}]] and [[{doc3_id}]]"}, headers={"Authorization": f"Bearer {token}"} ) await client.post( f"/api/v1/documents/{doc2_id}/detect-links", json={"content": f"Links to [[{doc3_id}]]"}, headers={"Authorization": f"Bearer {token}"} ) # Check doc3's links - should have backlinks from doc1 and doc2 response = await client.get( f"/api/v1/documents/{doc3_id}/links", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["document_id"] == doc3_id assert len(data["backlinks"]) == 2 assert data["outgoing_links"] == [] # ============================================================================= # Project Graph Tests # ============================================================================= @pytest.mark.asyncio async def test_get_project_graph(client): """Test getting project graph.""" token, proj_id, doc1_id, doc2_id, doc3_id = await setup_project_documents(client) # Create link: doc1 -> doc2 await client.post( f"/api/v1/documents/{doc1_id}/detect-links", json={"content": f"See [[{doc2_id}]]"}, headers={"Authorization": f"Bearer {token}"} ) response = await client.get( f"/api/v1/projects/{proj_id}/graph", params={"depth": 2}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["project_id"] == proj_id assert data["stats"]["total_documents"] == 3 assert data["stats"]["total_references"] == 1 node_ids = [n["id"] for n in data["nodes"]] assert set(node_ids) == {doc1_id, doc2_id, doc3_id} edge_sources = [e["source"] for e in data["edges"]] assert doc1_id in edge_sources @pytest.mark.asyncio async def test_get_project_graph_depth(client): """Test that depth parameter works correctly.""" token, proj_id, doc1_id, doc2_id, doc3_id = await setup_project_documents(client) # doc1 -> doc2 -> doc3 await client.post( f"/api/v1/documents/{doc1_id}/detect-links", json={"content": f"Link to [[{doc2_id}]]"}, headers={"Authorization": f"Bearer {token}"} ) await client.post( f"/api/v1/documents/{doc2_id}/detect-links", json={"content": f"Link to [[{doc3_id}]]"}, headers={"Authorization": f"Bearer {token}"} ) response = await client.get( f"/api/v1/projects/{proj_id}/graph", params={"depth": 3}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["stats"]["total_references"] == 2 @pytest.mark.asyncio async def test_get_project_graph_orphaned(client): """Test orphaned documents detection.""" token, proj_id, doc1_id, _, _ = await setup_project_documents(client) # All docs have no links between them - all 3 are orphaned response = await client.get( f"/api/v1/projects/{proj_id}/graph", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() # All 3 documents have no incoming AND no outgoing links assert data["stats"]["orphaned_documents"] == 3 # ============================================================================= # Quick Switcher Tests # ============================================================================= @pytest.mark.asyncio async def test_quick_switcher_documents(client): """Test Quick Switcher searching documents.""" token, proj_id, doc1_id, doc2_id, doc3_id = await setup_project_documents(client) response = await client.get( "/api/v1/search/quick", params={"q": "Document", "type": "documents", "limit": 10}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["query"] == "Document" assert data["search_type"] == "fuzzy" assert len(data["results"]) == 3 for item in data["results"]: assert item["type"] == "document" assert item["icon"] == "📄" @pytest.mark.asyncio async def test_quick_switcher_projects(client): """Test Quick Switcher searching projects.""" token, _, _, _, _ = await setup_project_documents(client) response = await client.get( "/api/v1/search/quick", params={"q": "Link", "type": "projects", "limit": 10}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["search_type"] == "fuzzy" assert len(data["results"]) == 1 assert data["results"][0]["type"] == "project" assert data["results"][0]["icon"] == "📁" @pytest.mark.asyncio async def test_quick_switcher_all_types(client): """Test Quick Switcher with type=all.""" token, proj_id, doc1_id, _, _ = await setup_project_documents(client) # Search for "Test" which should match the project name "Link Test Project" # and also potentially documents response = await client.get( "/api/v1/search/quick", params={"q": "Test", "type": "all", "limit": 10}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() types = [r["type"] for r in data["results"]] # Should include at least the project assert "project" in types @pytest.mark.asyncio async def test_quick_switcher_with_highlight(client): """Test that results include highlight markup.""" token, proj_id, doc1_id, _, _ = await setup_project_documents(client) response = await client.get( "/api/v1/search/quick", params={"q": "Document", "type": "documents", "limit": 10}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() for item in data["results"]: assert "" in (item["highlight"] or "") @pytest.mark.asyncio async def test_quick_switcher_project_filter(client): """Test Quick Switcher filtered by project.""" token, proj_id, doc1_id, _, _ = await setup_project_documents(client) response = await client.get( f"/api/v1/search/quick", params={"q": "Document", "type": "documents", "project_id": proj_id, "limit": 10}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() for item in data["results"]: assert item["project_id"] == proj_id @pytest.mark.asyncio async def test_quick_switcher_query_too_long(client): """Test that queries over 200 chars are rejected.""" token, _, _, _, _ = await setup_project_documents(client) long_query = "a" * 201 response = await client.get( "/api/v1/search/quick", params={"q": long_query}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 400 # ============================================================================= # Project Document Search Tests # ============================================================================= @pytest.mark.asyncio async def test_search_project_documents(client): """Test searching within a project's documents.""" token, proj_id, doc1_id, doc2_id, doc3_id = await setup_project_documents(client) response = await client.get( f"/api/v1/projects/{proj_id}/documents/search", params={"q": "second"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["project_id"] == proj_id assert data["query"] == "second" assert data["total"] == 1 assert data["results"][0]["title"] == "Document Two" @pytest.mark.asyncio async def test_search_project_documents_no_results(client): """Test search with no matching documents.""" token, proj_id, _, _, _ = await setup_project_documents(client) response = await client.get( f"/api/v1/projects/{proj_id}/documents/search", params={"q": "nonexistent xyz"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 data = response.json() assert data["results"] == [] # ============================================================================= # Export Tests # ============================================================================= @pytest.mark.asyncio async def test_export_document_markdown(client): """Test exporting a document as Markdown.""" token, proj_id, doc1_id, _, _ = await setup_project_documents(client) # Update doc content await client.put( f"/api/v1/documents/{doc1_id}/content", json={"content": "# Hello\n\nWorld content"}, headers={"Authorization": f"Bearer {token}"} ) response = await client.get( f"/api/v1/documents/{doc1_id}/export", params={"format": "markdown"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 assert "text/markdown" in response.headers["content-type"] assert "Hello" in response.text assert "World content" in response.text @pytest.mark.asyncio async def test_export_document_json(client): """Test exporting a document as JSON.""" token, proj_id, doc1_id, _, _ = await setup_project_documents(client) response = await client.get( f"/api/v1/documents/{doc1_id}/export", params={"format": "json"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 assert "application/json" in response.headers["content-type"] data = response.json() assert data["id"] == doc1_id assert data["title"] == "Document One" assert "metadata" in data @pytest.mark.asyncio async def test_export_project_json(client): """Test exporting a project as JSON.""" token, proj_id, doc1_id, doc2_id, doc3_id = await setup_project_documents(client) response = await client.get( f"/api/v1/projects/{proj_id}/export", params={"format": "json"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 assert "application/json" in response.headers["content-type"] data = response.json() assert data["project"]["id"] == proj_id assert len(data["documents"]) == 3 assert data["format_version"] == "3.0" @pytest.mark.asyncio async def test_export_project_zip(client): """Test exporting a project as ZIP.""" token, proj_id, doc1_id, doc2_id, _ = await setup_project_documents(client) response = await client.get( f"/api/v1/projects/{proj_id}/export", params={"format": "zip"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 assert "application/zip" in response.headers["content-type"] assert ".zip" in response.headers["content-disposition"] @pytest.mark.asyncio async def test_export_document_not_found(client): """Test export returns 404 for non-existent document.""" await client.post("/api/v1/auth/register", json={"username": "exportuser2", "password": "pass123"}) login = await client.post("/api/v1/auth/login", json={"username": "exportuser2", "password": "pass123"}) token = login.json()["access_token"] fake_id = str(uuid.uuid4()) response = await client.get( f"/api/v1/documents/{fake_id}/export", params={"format": "markdown"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 404 @pytest.mark.asyncio async def test_export_project_not_found(client): """Test export returns 404 for non-existent project.""" await client.post("/api/v1/auth/register", json={"username": "exportuser3", "password": "pass123"}) login = await client.post("/api/v1/auth/login", json={"username": "exportuser3", "password": "pass123"}) token = login.json()["access_token"] fake_id = str(uuid.uuid4()) response = await client.get( f"/api/v1/projects/{fake_id}/export", params={"format": "json"}, headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 404