Two unrelated CI failures on this branch, one fix each: - tests/integration/test_deck_vector_search.py: pass str(card.id) to get_chunk_with_context. The function's contract is doc_id: str (keyword-indexed in Qdrant), and real callers (viz_routes.py URL path, server/semantic.py via str(result.id)) all stringify. The test was the only int caller, hitting the .isdigit() guard added earlier on this branch. - tests/server/login_flow/test_login_flow_integration.py: test_check_status_provisioned now accepts scopes=None as valid. Per ProvisionStatusResponse in models/auth.py, None is the documented sentinel for "all scopes granted" — and the web provisioning path (provision_routes.py, used by Astrolabe's "Enable Semantic Search" flow exercised by the new regression test added on this branch) stores exactly that. The previous is-not-None assertion hid behind test order until that flow ran. - Replace anyio.sleep(0) with anyio.lowlevel.checkpoint() Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
242 lines
8.8 KiB
Python
242 lines
8.8 KiB
Python
"""Integration tests for Deck card vector search.
|
|
|
|
These tests validate that Deck cards are properly indexed and searchable
|
|
via semantic search.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
pytestmark = [pytest.mark.integration, pytest.mark.smoke]
|
|
|
|
|
|
async def test_deck_card_semantic_search(nc_mcp_client, nc_client, mocker):
|
|
"""Test that Deck cards can be indexed and searched via semantic search.
|
|
|
|
This test:
|
|
1. Creates a Deck board with a card
|
|
2. Manually triggers indexing (simulates vector sync)
|
|
3. Performs semantic search filtering by deck_card doc_type
|
|
4. Verifies the card is found in results
|
|
"""
|
|
# Skip if vector sync is not enabled
|
|
settings_response = await nc_mcp_client.call_tool("nc_get_vector_sync_status", {})
|
|
if settings_response.isError:
|
|
pytest.skip("Vector sync not enabled")
|
|
|
|
# Create a test board
|
|
board_title = "Test Board for Vector Search"
|
|
board = await nc_client.deck.create_board(title=board_title, color="ff0000")
|
|
|
|
try:
|
|
# Create a stack for the board
|
|
stack = await nc_client.deck.create_stack(
|
|
board_id=board.id, title="Test Stack", order=0
|
|
)
|
|
|
|
# Create a test card with searchable content
|
|
card_title = "Machine Learning Project Plan"
|
|
card_description = """
|
|
# ML Project Outline
|
|
|
|
## Phase 1: Data Collection
|
|
- Gather training data from multiple sources
|
|
- Clean and preprocess the dataset
|
|
|
|
## Phase 2: Model Training
|
|
- Experiment with different neural network architectures
|
|
- Use gradient descent optimization
|
|
|
|
## Phase 3: Deployment
|
|
- Deploy model to production environment
|
|
- Monitor performance metrics
|
|
"""
|
|
card = await nc_client.deck.create_card(
|
|
board_id=board.id,
|
|
stack_id=stack.id,
|
|
title=card_title,
|
|
description=card_description,
|
|
)
|
|
|
|
# Note: In a real integration test with vector sync enabled,
|
|
# we would wait for the background scanner to index the card.
|
|
# For now, we'll test the scanning function directly if needed.
|
|
|
|
# TODO: Once vector sync is running in test environment,
|
|
# add actual semantic search test here
|
|
# For now, just verify the card was created successfully
|
|
assert card.id is not None
|
|
assert card.title == card_title
|
|
assert card.description == card_description
|
|
|
|
# Test semantic search with deck_card filter
|
|
# Note: This will only work if vector sync is actually running
|
|
# and the card has been indexed
|
|
try:
|
|
search_result = await nc_mcp_client.call_tool(
|
|
"nc_semantic_search",
|
|
{
|
|
"query": "machine learning neural networks",
|
|
"doc_types": ["deck_card"],
|
|
"limit": 10,
|
|
},
|
|
)
|
|
|
|
# If vector sync is working, we should find the card
|
|
if not search_result.isError:
|
|
data = search_result.structuredContent
|
|
results = data.get("results", [])
|
|
|
|
# Check if our card is in the results
|
|
found_card = any(
|
|
r.get("doc_type") == "deck_card" and r.get("title") == card_title
|
|
for r in results
|
|
)
|
|
|
|
# Log result for debugging
|
|
if found_card:
|
|
print("✓ Successfully found Deck card in vector search")
|
|
else:
|
|
print(
|
|
"⚠ Deck card not found in search (may need time for indexing)"
|
|
)
|
|
except Exception as e:
|
|
# If search fails, it might be because indexing hasn't happened yet
|
|
print(f"⚠ Semantic search failed (indexing may not be complete): {e}")
|
|
|
|
finally:
|
|
# Cleanup: delete the board
|
|
try:
|
|
await nc_client.deck.delete_board(board.id)
|
|
except Exception as e:
|
|
print(f"Warning: Failed to cleanup test board: {e}")
|
|
|
|
|
|
async def test_deck_card_appears_in_cross_app_search(nc_mcp_client, nc_client):
|
|
"""Test that Deck cards appear in cross-app semantic search (no doc_type filter).
|
|
|
|
This verifies that when searching without specifying doc_types,
|
|
Deck cards are included in the results alongside notes, files, etc.
|
|
"""
|
|
# Skip if vector sync is not enabled
|
|
settings_response = await nc_mcp_client.call_tool("nc_get_vector_sync_status", {})
|
|
if settings_response.isError:
|
|
pytest.skip("Vector sync not enabled")
|
|
|
|
# Create a test board with a distinctive card
|
|
board_title = "Cross-App Search Test Board"
|
|
board = await nc_client.deck.create_board(title=board_title, color="00ff00")
|
|
|
|
try:
|
|
# Create a stack for the board
|
|
stack = await nc_client.deck.create_stack(
|
|
board_id=board.id, title="Test Stack", order=0
|
|
)
|
|
|
|
# Use a very distinctive term to make it easy to find
|
|
unique_term = "xylophone_banana_unicorn_test"
|
|
_card = await nc_client.deck.create_card(
|
|
board_id=board.id,
|
|
stack_id=stack.id,
|
|
title=f"Test Card with {unique_term}",
|
|
description=f"This card contains the unique search term: {unique_term}",
|
|
)
|
|
|
|
# Test cross-app search (no doc_type filter)
|
|
try:
|
|
search_result = await nc_mcp_client.call_tool(
|
|
"nc_semantic_search",
|
|
{
|
|
"query": unique_term,
|
|
"limit": 20,
|
|
},
|
|
)
|
|
|
|
if not search_result.isError:
|
|
data = search_result.structuredContent
|
|
results = data.get("results", [])
|
|
|
|
# Check if deck_card appears in cross-app results
|
|
deck_cards_found = [
|
|
r for r in results if r.get("doc_type") == "deck_card"
|
|
]
|
|
|
|
if deck_cards_found:
|
|
print(
|
|
f"✓ Found {len(deck_cards_found)} Deck card(s) in cross-app search"
|
|
)
|
|
else:
|
|
print(
|
|
"⚠ No Deck cards in cross-app search (may need time for indexing)"
|
|
)
|
|
except Exception as e:
|
|
print(f"⚠ Cross-app search failed: {e}")
|
|
|
|
finally:
|
|
# Cleanup
|
|
try:
|
|
await nc_client.deck.delete_board(board.id)
|
|
except Exception as e:
|
|
print(f"Warning: Failed to cleanup test board: {e}")
|
|
|
|
|
|
async def test_deck_card_chunk_context(nc_client):
|
|
"""Test that Deck card chunk context can be fetched for visualization.
|
|
|
|
This test validates that the vector viz UI can display Deck card previews
|
|
by fetching the chunk context via the context expansion module.
|
|
"""
|
|
from nextcloud_mcp_server.search.context import get_chunk_with_context
|
|
|
|
# Create board, stack, and card
|
|
board = await nc_client.deck.create_board(title="Test Board", color="ff0000")
|
|
|
|
try:
|
|
stack = await nc_client.deck.create_stack(
|
|
board_id=board.id, title="Test Stack", order=0
|
|
)
|
|
|
|
card_title = "Test Card for Context Expansion"
|
|
card_description = "This is a test description that should be fetched by the context expansion module when displaying chunk previews in the vector visualization UI."
|
|
|
|
card = await nc_client.deck.create_card(
|
|
board_id=board.id,
|
|
stack_id=stack.id,
|
|
title=card_title,
|
|
description=card_description,
|
|
)
|
|
|
|
# Fetch chunk context (simulates viz UI request)
|
|
# The chunk spans the title, so start=0 and end=len(card_title)
|
|
# doc_id is str — keyword-indexed in Qdrant payload; the real
|
|
# callers (viz_routes.py from URL path; server/semantic.py from
|
|
# str(result.id)) all stringify before reaching this entry point.
|
|
context = await get_chunk_with_context(
|
|
nc_client=nc_client,
|
|
user_id=nc_client.username,
|
|
doc_id=str(card.id),
|
|
doc_type="deck_card",
|
|
chunk_start=0,
|
|
chunk_end=len(card_title),
|
|
context_chars=100,
|
|
)
|
|
|
|
# Verify context was fetched successfully
|
|
assert context is not None, "Chunk context should not be None"
|
|
assert card_title in context.chunk_text, (
|
|
f"Card title '{card_title}' should be in chunk_text"
|
|
)
|
|
|
|
# Verify context includes description
|
|
assert card_description[:50] in context.after_context, (
|
|
"Card description should be in after_context"
|
|
)
|
|
|
|
print(f"✓ Successfully fetched chunk context for Deck card {card.id}")
|
|
|
|
finally:
|
|
# Cleanup
|
|
try:
|
|
await nc_client.deck.delete_board(board.id)
|
|
except Exception as e:
|
|
print(f"Warning: Failed to cleanup test board: {e}")
|