From 1a57f97d3a21daa445225abf1587a6a80630f5da Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Sat, 8 Nov 2025 22:41:14 +0100 Subject: [PATCH] refactor: update to Qdrant query_points API and fix Playwright Keycloak login - Replace deprecated qdrant_client.search() with query_points() API - Update semantic search implementation in notes.py - Update all integration tests to use query_points() - Fix Keycloak login in test_keycloak_dcr.py to use form.submit() instead of button click - Remove unnecessary popup handler code - Simplify consent screen logging --- nextcloud_mcp_server/server/notes.py | 6 ++-- tests/integration/test_semantic_search.py | 40 +++++++++++------------ tests/server/oauth/test_keycloak_dcr.py | 18 +++++----- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/nextcloud_mcp_server/server/notes.py b/nextcloud_mcp_server/server/notes.py index 5a54aaa..22ec661 100644 --- a/nextcloud_mcp_server/server/notes.py +++ b/nextcloud_mcp_server/server/notes.py @@ -414,9 +414,9 @@ def configure_notes_tools(mcp: FastMCP): # Search Qdrant with user filtering qdrant_client = await get_qdrant_client() - search_results = await qdrant_client.search( + search_response = await qdrant_client.query_points( collection_name=settings.qdrant_collection, - query_vector=query_embedding, + query=query_embedding, query_filter=Filter( must=[ FieldCondition( @@ -439,7 +439,7 @@ def configure_notes_tools(mcp: FastMCP): seen_note_ids = set() results = [] - for result in search_results: + for result in search_response.points: note_id = int(result.payload["doc_id"]) # Skip if we've already seen this note diff --git a/tests/integration/test_semantic_search.py b/tests/integration/test_semantic_search.py index 09f9d5e..17ab66a 100644 --- a/tests/integration/test_semantic_search.py +++ b/tests/integration/test_semantic_search.py @@ -207,32 +207,32 @@ async def test_semantic_search_with_qdrant( query = "async programming patterns in Python" query_embedding = await simple_embedding_provider.embed(query) - results = await qdrant_test_client.search( + response = await qdrant_test_client.query_points( collection_name=test_collection, - query_vector=query_embedding, + query=query_embedding, limit=3, score_threshold=0.0, ) # Should find Python note as top result - assert len(results) > 0 - assert results[0].payload["note_id"] == 1 - assert "Python" in results[0].payload["title"] + assert len(response.points) > 0 + assert response.points[0].payload["note_id"] == 1 + assert "Python" in response.points[0].payload["title"] # Test Query 2: Search for books query = "good books to read recommendations" query_embedding = await simple_embedding_provider.embed(query) - results = await qdrant_test_client.search( + response = await qdrant_test_client.query_points( collection_name=test_collection, - query_vector=query_embedding, + query=query_embedding, limit=3, score_threshold=0.0, ) # Should find book recommendations note - assert len(results) > 0 - top_result = results[0] + assert len(response.points) > 0 + top_result = response.points[0] assert top_result.payload["note_id"] == 2 assert "Book" in top_result.payload["title"] @@ -240,17 +240,17 @@ async def test_semantic_search_with_qdrant( query = "how to bake cookies dessert" query_embedding = await simple_embedding_provider.embed(query) - results = await qdrant_test_client.search( + response = await qdrant_test_client.query_points( collection_name=test_collection, - query_vector=query_embedding, + query=query_embedding, limit=3, score_threshold=0.0, ) # Should find recipe note - assert len(results) > 0 + assert len(response.points) > 0 # Recipe should be in top 2 results - top_note_ids = [r.payload["note_id"] for r in results[:2]] + top_note_ids = [r.payload["note_id"] for r in response.points[:2]] assert 3 in top_note_ids @@ -289,9 +289,9 @@ async def test_semantic_search_with_filters( query = "books reading" query_embedding = await simple_embedding_provider.embed(query) - results = await qdrant_test_client.search( + response = await qdrant_test_client.query_points( collection_name=test_collection, - query_vector=query_embedding, + query=query_embedding, query_filter=Filter( must=[FieldCondition(key="category", match=MatchValue(value="Personal"))] ), @@ -299,8 +299,8 @@ async def test_semantic_search_with_filters( ) # Should only return Personal category notes - assert len(results) > 0 - for result in results: + assert len(response.points) > 0 + for result in response.points: assert result.payload["category"] == "Personal" @@ -314,13 +314,13 @@ async def test_semantic_search_empty_results( query = "test query" query_embedding = await simple_embedding_provider.embed(query) - results = await qdrant_test_client.search( + response = await qdrant_test_client.query_points( collection_name=test_collection, - query_vector=query_embedding, + query=query_embedding, limit=10, ) - assert len(results) == 0 + assert len(response.points) == 0 async def test_batch_embedding(simple_embedding_provider: SimpleEmbeddingProvider): diff --git a/tests/server/oauth/test_keycloak_dcr.py b/tests/server/oauth/test_keycloak_dcr.py index b827c41..c88ea1d 100644 --- a/tests/server/oauth/test_keycloak_dcr.py +++ b/tests/server/oauth/test_keycloak_dcr.py @@ -46,9 +46,10 @@ async def handle_keycloak_login(page, username: str, password: str): Keycloak uses: - input#username for username field - input#password for password field - - input[type="submit"] for submit button + - Form submission via JavaScript (more reliable than clicking button) """ logger.info(f"Handling Keycloak login for user: {username}") + logger.info(f"Current URL before login: {page.url}") # Wait for username field and fill it await page.wait_for_selector("input#username", timeout=10000) @@ -58,11 +59,12 @@ async def handle_keycloak_login(page, username: str, password: str): await page.wait_for_selector("input#password", timeout=10000) await page.fill("input#password", password) - # Click submit button - await page.click('input[type="submit"]') - await page.wait_for_load_state("networkidle", timeout=60000) + # Submit form using JavaScript (more reliable than clicking button) + logger.info("Submitting Keycloak login form...") + async with page.expect_navigation(timeout=60000): + await page.evaluate("document.querySelector('form').submit()") - logger.info("✓ Keycloak login completed") + logger.info(f"✓ Keycloak login completed, redirected to: {page.url}") async def handle_keycloak_consent(page, client_name: str): @@ -80,9 +82,9 @@ async def handle_keycloak_consent(page, client_name: str): # Wait for consent screen (button with name="accept") await page.wait_for_selector('button[name="accept"]', timeout=5000) - # Click accept button - await page.click('button[name="accept"]') - await page.wait_for_load_state("networkidle", timeout=60000) + # Click accept button and wait for navigation + async with page.expect_navigation(timeout=60000): + await page.click('button[name="accept"]') logger.info("✓ Keycloak consent granted") except Exception as e: