"""Tests for ColBERT-based card component retrieval and NLP parsing integration.
This module tests the ColBERT multi-vector semantic search functionality
for finding card components and validates that NLP parameter extraction
works correctly in both ColBERT and standard modes.
Key areas tested:
1. ColBERT search returns results with valid component objects
2. Component path lookup via _get_component_from_path
3. NLP parsing should extract parameters regardless of ColBERT mode
4. End-to-end card generation with ColBERT + NLP combined
"""
import json
import logging
import os
import sys
from datetime import datetime
from typing import Any, Dict, List, Optional
import pytest
from dotenv import load_dotenv
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Load environment variables
load_dotenv()
# Add project root to path for imports
sys.path.insert(
0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
)
# Test configuration
TEST_EMAIL = os.getenv("TEST_EMAIL_ADDRESS", "test@example.com")
TEST_WEBHOOK_URL = os.getenv("TEST_CHAT_WEBHOOK", "")
TEST_SPACE_ID = "AAAAWvjq2HE"
class TestColBERTComponentRetrieval:
"""Test ColBERT-based component retrieval functionality."""
@pytest.mark.asyncio
async def test_colbert_search_returns_results(self, client):
"""Test that ColBERT search finds card components."""
if not TEST_WEBHOOK_URL:
pytest.skip("TEST_CHAT_WEBHOOK not set")
# Send a card with use_colbert=true and check response
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": "A simple card with a header",
"card_params": {},
"webhook_url": TEST_WEBHOOK_URL,
"use_colbert": True,
}
result = await client.call_tool("send_dynamic_card", test_payload)
content = _extract_content(result)
print(f"\n{'=' * 60}")
print("ColBERT Search Results Test")
print(f"{'=' * 60}")
print(f"Response: {content[:500]}...")
# Parse the JSON response
try:
data = json.loads(content)
component_info = data.get("componentInfo", {})
print(f"\nComponent Info:")
print(f" - componentFound: {component_info.get('componentFound')}")
print(f" - componentName: {component_info.get('componentName')}")
print(f" - searchScore: {component_info.get('searchScore')}")
print(f" - componentType: {component_info.get('componentType')}")
# Check validation issues to understand what went wrong
validation_issues = data.get("validationIssues", [])
if validation_issues:
print(f"\nValidation Issues:")
for issue in validation_issues:
print(f" - {issue}")
# The card may fail if NLP wasn't run - that's the bug we're detecting
if data.get("nlpExtraction") is None and not data.get("success"):
print(
"\n*** NOTE: NLP extraction was null - this is expected with the current bug ***"
)
print(
"ColBERT found the component but no parameters were extracted to populate it."
)
except json.JSONDecodeError:
print(f"Non-JSON response: {content}")
@pytest.mark.asyncio
async def test_colbert_vs_standard_search_comparison(self, client):
"""Compare ColBERT vs standard search results for the same query."""
if not TEST_WEBHOOK_URL:
pytest.skip("TEST_CHAT_WEBHOOK not set")
description = "Create a card with header 'Test' and a button labeled 'Click Me'"
# Test with ColBERT
colbert_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": description,
"card_params": {},
"webhook_url": TEST_WEBHOOK_URL,
"use_colbert": True,
}
# Test without ColBERT (standard mode)
standard_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": description,
"card_params": {},
"webhook_url": TEST_WEBHOOK_URL,
"use_colbert": False,
}
colbert_result = await client.call_tool("send_dynamic_card", colbert_payload)
standard_result = await client.call_tool("send_dynamic_card", standard_payload)
colbert_content = _extract_content(colbert_result)
standard_content = _extract_content(standard_result)
print(f"\n{'=' * 60}")
print("ColBERT vs Standard Search Comparison")
print(f"{'=' * 60}")
try:
colbert_data = json.loads(colbert_content)
standard_data = json.loads(standard_content)
print(f"\nColBERT Mode:")
print(f" - Card Type: {colbert_data.get('cardType')}")
print(
f" - Component: {colbert_data.get('componentInfo', {}).get('componentName')}"
)
print(
f" - Score: {colbert_data.get('componentInfo', {}).get('searchScore')}"
)
print(f"\nStandard Mode:")
print(f" - Card Type: {standard_data.get('cardType')}")
print(
f" - Component: {standard_data.get('componentInfo', {}).get('componentName')}"
)
print(
f" - Score: {standard_data.get('componentInfo', {}).get('searchScore')}"
)
# Check if NLP was used in both modes (the key fix we're testing)
# Note: Card may still fail validation for complex descriptions,
# but NLP should run in both modes
colbert_validation = colbert_data.get("validationIssues") or []
standard_validation = standard_data.get("validationIssues") or []
# The key test: both should attempt card generation (not skip NLP)
# If both have same validation issues, NLP is working in both modes
print(f"\n ColBERT validation issues: {len(colbert_validation)}")
print(f" Standard validation issues: {len(standard_validation)}")
print(f"\n ColBERT success: {colbert_data.get('success')}")
print(f" Standard success: {standard_data.get('success')}")
except json.JSONDecodeError as e:
print(f"JSON decode error: {e}")
class TestNLPParsingWithColBERT:
"""Test that NLP parameter extraction works correctly with ColBERT mode."""
@pytest.mark.asyncio
async def test_nlp_extracts_parameters_in_colbert_mode(self, client):
"""
BUG TEST: Verify NLP parsing extracts parameters even when ColBERT is enabled.
Current bug: ColBERT mode skips NLP parsing entirely (see card_tools.py)
Expected: NLP should extract header, button text, URLs even in ColBERT mode
"""
if not TEST_WEBHOOK_URL:
pytest.skip("TEST_CHAT_WEBHOOK not set")
# Description with clear extractable parameters
description = "Create a card with header 'Project Status' and subtitle 'Weekly Update' and a green button labeled 'Approve' that opens https://example.com/approve"
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": description,
"card_params": {}, # Empty to force NLP extraction
"webhook_url": TEST_WEBHOOK_URL,
"use_colbert": True,
}
result = await client.call_tool("send_dynamic_card", test_payload)
content = _extract_content(result)
print(f"\n{'=' * 60}")
print("NLP Parameter Extraction in ColBERT Mode")
print(f"{'=' * 60}")
print(f"Description: {description}")
try:
data = json.loads(content)
print(f"\nResponse Analysis:")
print(f" - Success: {data.get('success')}")
print(f" - HTTP Status: {data.get('httpStatus')}")
print(f" - Card Type: {data.get('cardType')}")
print(f" - NLP Extraction: {data.get('nlpExtraction')}")
# Check if the card has actual content (not empty fallback)
message = data.get("message", "")
# BUG DETECTION: If we see "Empty card" that means NLP didn't extract params
if "Empty card" in message:
print("\n*** BUG DETECTED ***")
print("NLP extraction was skipped in ColBERT mode!")
print("The card fell back to empty content.")
pytest.fail(
"BUG: ColBERT mode skipped NLP parsing. "
"Card params should have been extracted from description but weren't. "
"See card_tools.py for details"
)
else:
print("\n NLP extraction appears to be working")
except json.JSONDecodeError:
print(f"Response: {content}")
@pytest.mark.asyncio
async def test_nlp_extraction_standard_mode_baseline(self, client):
"""Baseline test: Verify NLP extraction works in standard (non-ColBERT) mode."""
if not TEST_WEBHOOK_URL:
pytest.skip("TEST_CHAT_WEBHOOK not set")
description = "Create a card titled 'Hello World' with text 'This is a test message' and a blue button 'Learn More'"
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": description,
"card_params": {},
"webhook_url": TEST_WEBHOOK_URL,
"use_colbert": False, # Standard mode
}
result = await client.call_tool("send_dynamic_card", test_payload)
content = _extract_content(result)
print(f"\n{'=' * 60}")
print("NLP Extraction Baseline (Standard Mode)")
print(f"{'=' * 60}")
try:
data = json.loads(content)
print(f" - Success: {data.get('success')}")
print(f" - Card Type: {data.get('cardType')}")
print(f" - NLP Extraction: {data.get('nlpExtraction')}")
message = data.get("message", "")
if "Empty card" not in message:
print("\n Standard mode NLP extraction working")
else:
print("\n WARNING: Even standard mode produced empty card")
except json.JSONDecodeError:
print(f"Response: {content}")
class TestComponentPathLookup:
"""Test component path lookup functionality in ModuleWrapper."""
@pytest.mark.asyncio
async def test_colbert_component_has_valid_object(self, client):
"""
BUG TEST: Verify ColBERT search results include actual component objects.
Current bug: _get_component_from_path returns None because path not in self.components
Expected: Component should be retrievable from the path returned by ColBERT search
"""
if not TEST_WEBHOOK_URL:
pytest.skip("TEST_CHAT_WEBHOOK not set")
# Use a description that should match a specific component
description = "card with header"
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": description,
"card_params": {"title": "Test Title"}, # Provide explicit params
"webhook_url": TEST_WEBHOOK_URL,
"use_colbert": True,
}
result = await client.call_tool("send_dynamic_card", test_payload)
content = _extract_content(result)
print(f"\n{'=' * 60}")
print("Component Object Retrieval Test")
print(f"{'=' * 60}")
try:
data = json.loads(content)
component_info = data.get("componentInfo", {})
print(f"\nComponent Info:")
print(f" - Found: {component_info.get('componentFound')}")
print(f" - Name: {component_info.get('componentName')}")
print(f" - Path: {component_info.get('componentPath')}")
print(f" - Type: {component_info.get('componentType')}")
print(f" - Score: {component_info.get('searchScore')}")
# Check if component type indicates a fallback
if component_info.get("componentType") == "simple_fallback":
print("\n*** BUG DETECTED ***")
print("Component lookup failed - fell back to simple_fallback")
print(
"ColBERT found a match but couldn't retrieve the component object"
)
except json.JSONDecodeError:
print(f"Response: {content}")
class TestEndToEndColBERTCardGeneration:
"""End-to-end tests for ColBERT-based card generation."""
@pytest.mark.asyncio
async def test_complex_card_colbert_mode(self, client):
"""Test generating a complex card using ColBERT mode."""
if not TEST_WEBHOOK_URL:
pytest.skip("TEST_CHAT_WEBHOOK not set")
timestamp = datetime.now().strftime("%H:%M:%S")
description = f"""Create a status dashboard card with:
- Header titled 'System Status' with subtitle 'Last updated: {timestamp}'
- A section showing 'All Systems Operational' with a green check icon
- A button labeled 'View Details' that opens https://status.example.com
"""
test_payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": description,
"card_params": {},
"webhook_url": TEST_WEBHOOK_URL,
"use_colbert": True,
}
result = await client.call_tool("send_dynamic_card", test_payload)
content = _extract_content(result)
print(f"\n{'=' * 60}")
print("Complex Card Generation (ColBERT Mode)")
print(f"{'=' * 60}")
try:
data = json.loads(content)
print(f"\nResult:")
print(f" - Success: {data.get('success')}")
print(f" - HTTP Status: {data.get('httpStatus')}")
print(f" - Card Type: {data.get('cardType')}")
print(
f" - Component: {data.get('componentInfo', {}).get('componentName')}"
)
# Check the message for content
message = data.get("message", "")
if "Empty card" in message:
print("\n FAILED: Card has no content")
elif "200" in str(data.get("httpStatus")):
print("\n Card sent successfully")
except json.JSONDecodeError:
print(f"Response: {content}")
@pytest.mark.asyncio
async def test_simple_text_card_colbert_vs_standard(self, client):
"""Compare simple text card generation between ColBERT and standard modes."""
if not TEST_WEBHOOK_URL:
pytest.skip("TEST_CHAT_WEBHOOK not set")
description = "A simple card with text saying 'Hello from ColBERT test'"
results = {}
for mode_name, use_colbert in [("ColBERT", True), ("Standard", False)]:
payload = {
"user_google_email": TEST_EMAIL,
"space_id": TEST_SPACE_ID,
"card_description": description,
"card_params": {},
"webhook_url": TEST_WEBHOOK_URL,
"use_colbert": use_colbert,
}
result = await client.call_tool("send_dynamic_card", payload)
content = _extract_content(result)
try:
results[mode_name] = json.loads(content)
except json.JSONDecodeError:
results[mode_name] = {"raw": content}
print(f"\n{'=' * 60}")
print("Simple Text Card: ColBERT vs Standard")
print(f"{'=' * 60}")
for mode, data in results.items():
print(f"\n{mode} Mode:")
if "raw" in data:
print(f" Raw response: {data['raw'][:200]}")
else:
print(f" - HTTP Status: {data.get('httpStatus')}")
print(f" - Card Type: {data.get('cardType')}")
has_content = "Empty card" not in data.get("message", "")
print(f" - Has Content: {has_content}")
# Helper function
def _extract_content(result) -> str:
"""Extract text content from tool result."""
if hasattr(result, "content"):
content_items = (
result.content if hasattr(result.content, "__iter__") else [result.content]
)
return (
content_items[0].text
if hasattr(content_items[0], "text")
else str(content_items[0])
)
return str(result)
if __name__ == "__main__":
pytest.main(["-xvs", __file__])