Skip to content

Cogniverse Study Guide: System Integration Module

Module Path: tests/system/, tests/e2e/


Module Overview

Purpose

The System Integration module validates Cogniverse's layered architecture:

  • End-to-End Workflows: Complete user query to result flows across all layers

  • Component Integration: Multi-agent communication and coordination

  • Backend Integration: Vespa, Phoenix, Mem0 connectivity

  • Package Isolation: Each package's integration with dependencies

  • Real System Testing: Production-like environment validation

Package Architecture Integration Testing

graph TD
    subgraph Foundation["<span style='color:#000'>Foundation Layer</span>"]
        SDK["<span style='color:#000'>cogniverse-sdk<br/>Interface contracts and document models</span>"]
        FOUND["<span style='color:#000'>cogniverse-foundation<br/>Config and telemetry base classes</span>"]
    end

    subgraph Core["<span style='color:#000'>Core Layer</span>"]
        CORE["<span style='color:#000'>cogniverse-core<br/>Agent base classes, memory, common utilities</span>"]
        EVAL["<span style='color:#000'>cogniverse-evaluation<br/>Experiment management and metrics</span>"]
        TELEM["<span style='color:#000'>cogniverse-telemetry-phoenix<br/>Phoenix telemetry provider (plugin)</span>"]
    end

    subgraph Implementation["<span style='color:#000'>Implementation Layer</span>"]
        AGENTS["<span style='color:#000'>cogniverse-agents<br/>Routing, search agents, orchestration</span>"]
        VESPA["<span style='color:#000'>cogniverse-vespa<br/>Vespa backend and tenant schema management</span>"]
        SYNTH["<span style='color:#000'>cogniverse-synthetic<br/>Synthetic data generation</span>"]
        FINE["<span style='color:#000'>cogniverse-finetuning<br/>LLM fine-tuning with LoRA/PEFT and DPO</span>"]
    end

    subgraph Application["<span style='color:#000'>Application Layer</span>"]
        RUNTIME["<span style='color:#000'>cogniverse-runtime<br/>FastAPI server and ingestion pipelines</span>"]
        CLI["<span style='color:#000'>cogniverse-cli<br/>Deployment and cluster management CLI</span>"]
        MSG["<span style='color:#000'>cogniverse-messaging<br/>Telegram/Slack messaging gateway</span>"]
    end

    CORE --> SDK
    CORE --> FOUND
    EVAL --> CORE
    TELEM --> FOUND
    AGENTS --> CORE
    VESPA --> CORE
    SYNTH --> CORE
    FINE --> CORE
    RUNTIME --> AGENTS
    RUNTIME --> VESPA
    CLI -.->|HTTP| RUNTIME
    MSG -.->|HTTP| RUNTIME

    style Foundation fill:#b0bec5,stroke:#546e7a,color:#000
    style Core fill:#ce93d8,stroke:#7b1fa2,color:#000
    style Implementation fill:#81d4fa,stroke:#0288d1,color:#000
    style Application fill:#a5d6a7,stroke:#388e3c,color:#000
    style SDK fill:#90caf9,stroke:#1565c0,color:#000
    style FOUND fill:#90caf9,stroke:#1565c0,color:#000
    style CORE fill:#ce93d8,stroke:#7b1fa2,color:#000
    style EVAL fill:#ce93d8,stroke:#7b1fa2,color:#000
    style TELEM fill:#ce93d8,stroke:#7b1fa2,color:#000
    style AGENTS fill:#81d4fa,stroke:#0288d1,color:#000
    style VESPA fill:#81d4fa,stroke:#0288d1,color:#000
    style SYNTH fill:#81d4fa,stroke:#0288d1,color:#000
    style FINE fill:#81d4fa,stroke:#0288d1,color:#000
    style RUNTIME fill:#a5d6a7,stroke:#388e3c,color:#000
    style CLI fill:#a5d6a7,stroke:#388e3c,color:#000
    style MSG fill:#a5d6a7,stroke:#388e3c,color:#000

The workspace has 12 packages total (libs/*); all are shown above. cogniverse-cli and cogniverse-messaging have no internal cogniverse-* package dependencies — they talk to the runtime over HTTP rather than by import.

Test Categories

System Integration Tests (tests/system/): - test_ensemble_search_e2e.py - Ensemble search RRF-fusion plumbing across multiple Vespa schemas - test_ensemble_comprehensive.py - Ensemble search against real ColPali/X-CLIP/ColQwen profiles

Agent End-to-End Tests (tests/e2e/, ~50 files, real LLM + real Vespa/Phoenix, no mocks): - test_api_e2e.py - Gateway/orchestrator REST routing and downstream execution - test_a2a_gateway_e2e.py, test_a2a_multiturn_e2e.py - A2A JSON-RPC and multi-turn conversations - test_orchestrator_inbound_e2e.py - Orchestrator planning and A2A fan-out - test_citation_and_audit_agents_e2e.py, test_contradiction_reconciliation_agent_e2e.py, test_cross_tenant_comparison_agent_e2e.py, test_federated_query_agent_e2e.py, test_kg_traversal_agent_e2e.py, test_knowledge_summarization_agent_e2e.py, test_multi_document_synthesis_agent_e2e.py, test_temporal_reasoning_agent_e2e.py - knowledge/audit agent tier - test_messaging_e2e.py, test_messaging_gateway_e2e.py - Telegram/Slack gateway integration - test_wiki_e2e.py, test_provenance_e2e.py, test_trust_ranking_e2e.py, test_pinning_quotas_e2e.py - memory/knowledge layer - See tests/e2e/ for the complete set

Note: tests/agents/e2e/ contains only test configuration (test_config.py) — the real agent end-to-end suite lives in tests/e2e/.


Integration Test Patterns

1. Full System Integration

class TestRealVespaIntegration:
    """End-to-end system validation"""

    async def test_comprehensive_agentic_system_test(self, vespa_test_manager):
        # 1. Setup: Initialize all components across packages
        from cogniverse_foundation.config.utils import create_default_config_manager
        from cogniverse_agents.gateway_agent import GatewayAgent, GatewayDeps, GatewayInput
        from cogniverse_agents.search_agent import SearchAgent, SearchAgentDeps

        tenant_id = "test"
        config_manager = create_default_config_manager()

        # cogniverse-vespa package
        from pathlib import Path
        from cogniverse_vespa.search_backend import VespaSearchBackend
        from cogniverse_core.schemas.filesystem_loader import FilesystemSchemaLoader

        schema_loader = FilesystemSchemaLoader(Path("configs/schemas"))
        search_backend = VespaSearchBackend(
            config={
                "url": "http://localhost",
                "port": 8080,
                "profiles": {},
                "default_profiles": {},
            },
            config_manager=config_manager,
            schema_loader=schema_loader,
        )

        # cogniverse-agents package - GatewayAgent classifies simple vs.
        # complex queries with no LLM call (GLiNER + deterministic rules)
        # and routes simple queries directly to an execution agent.
        gateway = GatewayAgent(deps=GatewayDeps())

        # SearchAgent requires SearchAgentDeps with backend connection details
        # Note: profile and tenant_id are passed per-request at search() time, not at construction
        search_deps = SearchAgentDeps(
            backend_url="http://localhost",
            backend_port=8080,
        )
        # schema_loader is REQUIRED (raises ValueError if None)
        # config_manager is optional (creates default if None)
        search_agent = SearchAgent(
            deps=search_deps,
            schema_loader=schema_loader,
            config_manager=config_manager
        )

        # 2. Query Processing
        user_query = "Show me cooking videos"

        # 3. Routing Decision via GatewayAgent
        routing_result = await gateway._process_impl(
            GatewayInput(query=user_query, tenant_id=tenant_id)
        )
        assert routing_result.routed_to == "search_agent"
        assert routing_result.confidence > 0.7

        # 4. Agent Execution (SearchAgent uses search_by_text method)
        # Returns List[Dict[str, Any]] with search results
        search_results = search_agent.search_by_text(
            query=user_query,
            tenant_id=tenant_id,
            modality="video",
            top_k=10
        )

        # 5. Validation
        assert len(search_results) > 0
        # Results are list of dicts with id, score, plus metadata (video_id, etc.) at top level
        assert "id" in search_results[0]
        assert "score" in search_results[0]
        assert "video_id" in search_results[0]

2. Multi-Agent Orchestration

# Example: Use OrchestratorAgent as the central A2A entry point
from cogniverse_agents.orchestrator_agent import OrchestratorAgent, OrchestratorDeps, OrchestratorInput
from cogniverse_core.registries.agent_registry import AgentRegistry
from cogniverse_foundation.config.utils import create_default_config_manager

tenant_id = "test_tenant"
config_manager = create_default_config_manager()

# OrchestratorAgent discovers agents via AgentRegistry (from config.json > agents section)
registry = AgentRegistry(tenant_id=tenant_id, config_manager=config_manager)
deps = OrchestratorDeps()
orchestrator = OrchestratorAgent(deps=deps, registry=registry, config_manager=config_manager)

# Execute via A2A task protocol
input_data = OrchestratorInput(query="Show me cooking videos", tenant_id=tenant_id)
result = await orchestrator._process_impl(input_data)

# Validate execution — OrchestratorOutput carries the plan, per-agent
# results, and the fused final_output (no top-level "result" field)
assert result is not None
assert result.workflow_id
assert result.final_output
assert result.agent_results

3. Backend Integration

# Example: Test Vespa connection via VespaSearchBackend
from pathlib import Path
from cogniverse_vespa.search_backend import VespaSearchBackend
from cogniverse_core.schemas.filesystem_loader import FilesystemSchemaLoader
from cogniverse_foundation.config.utils import create_default_config_manager

config_manager = create_default_config_manager()
schema_loader = FilesystemSchemaLoader(Path("configs/schemas"))
search_backend = VespaSearchBackend(
    config={
        "url": "http://localhost",
        "port": 8080,
        "profiles": {},
        "default_profiles": {},
    },
    config_manager=config_manager,
    schema_loader=schema_loader,
)

# Execute search query. strategy is a rank-profile name string (e.g.
# "bm25_only"); tenant_id is required.
results = search_backend.search(
    {
        "query": "cooking tutorial",
        "type": "video",
        "strategy": "bm25_only",
        "top_k": 10,
        "tenant_id": "acme:prod",
    }
)

# Validate results (list of SearchResult with .score and .document)
assert len(results) > 0
for result in results:
    assert result.score is not None
    assert result.document.metadata["source_id"]

4. Telemetry Integration

# Example: Phoenix telemetry validation
from cogniverse_foundation.telemetry.manager import get_telemetry_manager

telemetry_manager = get_telemetry_manager()

# Execute operation with span
with telemetry_manager.span("test_operation", tenant_id="test") as span:
    result = perform_operation()
    # Span is automatically recorded to Phoenix

# Telemetry data can be queried via Phoenix AsyncClient
# See Phoenix documentation for span query APIs

Common Integration Scenarios

Scenario 1: Video Search Workflow

flowchart LR
    Query["<span style='color:#000'>User Query</span>"]
    Routing["<span style='color:#000'>Routing Agent</span>"]
    Video["<span style='color:#000'>Video Agent</span>"]
    Vespa["<span style='color:#000'>Backend Search</span>"]
    Results["<span style='color:#000'>Results</span>"]
    Telemetry["<span style='color:#000'>Telemetry Spans</span>"]

    Query --> Routing
    Routing --> Video
    Video --> Vespa
    Vespa --> Results

    Query -.-> Telemetry
    Routing -.-> Telemetry
    Video -.-> Telemetry
    Vespa -.-> Telemetry
    Results -.-> Telemetry

    style Query fill:#90caf9,stroke:#1565c0,color:#000
    style Routing fill:#ce93d8,stroke:#7b1fa2,color:#000
    style Video fill:#ce93d8,stroke:#7b1fa2,color:#000
    style Vespa fill:#90caf9,stroke:#1565c0,color:#000
    style Results fill:#a5d6a7,stroke:#388e3c,color:#000
    style Telemetry fill:#a5d6a7,stroke:#388e3c,color:#000

Example:

# Example: Video search integration
query = "pasta cooking tutorial"
tenant_id = "test"

# Route via GatewayAgent (GLiNER classification, no LLM call)
route = await gateway._process_impl(GatewayInput(query=query, tenant_id=tenant_id))
assert route.routed_to == "search_agent"
assert route.confidence > 0.7

# Search (using SearchAgent - returns List[Dict[str, Any]])
results = search_agent.search_by_text(query, tenant_id=tenant_id, modality="video", top_k=10)
assert len(results) > 0

# Verify (results have id, score, plus metadata like video_id at top level)
assert all("id" in r for r in results)
assert all("score" in r for r in results)
assert results[0]["score"] > 0.0

Scenario 2: Multi-Modal Fusion

flowchart LR
    Query["<span style='color:#000'>User Query</span>"]
    Routing["<span style='color:#000'>Routing Agent</span>"]
    VideoAgent["<span style='color:#000'>Video Agent</span>"]
    TextAgent["<span style='color:#000'>Text Agent</span>"]
    Fusion["<span style='color:#000'>Result Fusion</span>"]
    Result["<span style='color:#000'>Final Result</span>"]

    Query --> Routing
    Routing --> VideoAgent
    Routing --> TextAgent
    VideoAgent --> Fusion
    TextAgent --> Fusion
    Fusion --> Result

    style Query fill:#90caf9,stroke:#1565c0,color:#000
    style Routing fill:#ce93d8,stroke:#7b1fa2,color:#000
    style VideoAgent fill:#ce93d8,stroke:#7b1fa2,color:#000
    style TextAgent fill:#ce93d8,stroke:#7b1fa2,color:#000
    style Fusion fill:#ffcc80,stroke:#ef6c00,color:#000
    style Result fill:#a5d6a7,stroke:#388e3c,color:#000

Example:

# Execute multimodal search using orchestrator
from cogniverse_agents.orchestrator_agent import OrchestratorAgent, OrchestratorDeps, OrchestratorInput
from cogniverse_core.registries.agent_registry import AgentRegistry
from cogniverse_foundation.config.utils import create_default_config_manager

query = "How does photosynthesis work?"
tenant_id = "test"

config_manager = create_default_config_manager()
registry = AgentRegistry(tenant_id=tenant_id, config_manager=config_manager)
deps = OrchestratorDeps()
orchestrator = OrchestratorAgent(deps=deps, registry=registry, config_manager=config_manager)

input_data = OrchestratorInput(query=query, tenant_id=tenant_id)
result = await orchestrator._process_impl(input_data)

# Validate orchestration occurred — final_output carries the fused
# cross-modal result, agent_results the per-agent raw outputs
assert result is not None
assert result.final_output
assert result.agent_results

Scenario 3: Memory-Enhanced Routing

flowchart LR
    Query["<span style='color:#000'>User Query</span>"]
    MemoryLookup["<span style='color:#000'>Memory Lookup</span>"]
    Context["<span style='color:#000'>Context</span>"]
    Routing["<span style='color:#000'>Routing Agent<br/>(Enhanced)</span>"]
    Agent["<span style='color:#000'>Target Agent</span>"]

    Query --> MemoryLookup
    MemoryLookup --> Context
    Context --> Routing
    Routing --> Agent

    style Query fill:#90caf9,stroke:#1565c0,color:#000
    style MemoryLookup fill:#90caf9,stroke:#1565c0,color:#000
    style Context fill:#b0bec5,stroke:#546e7a,color:#000
    style Routing fill:#ce93d8,stroke:#7b1fa2,color:#000
    style Agent fill:#ce93d8,stroke:#7b1fa2,color:#000

Example:

# Example: Memory-enhanced routing
from cogniverse_core.memory.manager import Mem0MemoryManager
from cogniverse_core.schemas.filesystem_loader import FilesystemSchemaLoader
from pathlib import Path

tenant_id = "user123"

# 1. Initialize and add memory. Mem0MemoryManager is a per-tenant singleton;
# initialize() must run once before add_memory()/get_relevant_context() are
# usable (agents normally do this via MemoryAwareMixin.initialize_memory,
# which wraps this same call with config-driven arguments).
memory_manager = Mem0MemoryManager(tenant_id=tenant_id)
memory_manager.initialize(
    backend_host="http://localhost",
    backend_port=8080,
    llm_model="qwen2.5:0.5b",
    embedding_model="lightonai/DenseOn",
    llm_base_url="http://localhost:11434",
    embedder_base_url="http://localhost:8001",
    config_manager=config_manager,
    schema_loader=FilesystemSchemaLoader(Path("configs/schemas")),
)
memory_manager.add_memory(
    content="User prefers video tutorials",
    tenant_id=tenant_id,
    agent_name="gateway_agent"
)

# 2. Route with memory (memory automatically loaded by MemoryAwareMixin
# on agents that mix it in, e.g. GatewayAgent's downstream execution agents)
route = await gateway._process_impl(
    GatewayInput(query="Show me how to cook", tenant_id=tenant_id)
)

# 3. Verify routing decision
assert route.routed_to == "search_agent"
assert route.confidence > 0.7


Production Testing

Load Testing

# Example: Concurrent query processing.
# Reuses `orchestrator` constructed as in "Multi-Agent Orchestration" above.
# `generate_test_queries` is a test-side helper returning a list of query strings.
import asyncio
import time

queries = generate_test_queries(100)

# Execute queries concurrently
async def process_single_query(query):
    start = time.time()
    result = await orchestrator._process_impl(OrchestratorInput(query=query, tenant_id="test"))
    latency = (time.time() - start) * 1000
    return {"status": "success", "latency_ms": latency, "result": result}

tasks = [process_single_query(q) for q in queries]
results = await asyncio.gather(*tasks)

# Validate
success_rate = sum(1 for r in results if r["status"] == "success") / len(results)
assert success_rate > 0.95

# Check latencies
latencies = [r["latency_ms"] for r in results]
p95_latency = sorted(latencies)[int(len(latencies) * 0.95)]
assert p95_latency < 1000  # < 1 second

Failure Recovery

# Example: Orchestrator handles failures
from cogniverse_agents.orchestrator_agent import OrchestratorAgent, OrchestratorDeps, OrchestratorInput
from cogniverse_core.registries.agent_registry import AgentRegistry
from cogniverse_foundation.config.utils import create_default_config_manager

tenant_id = "test"
config_manager = create_default_config_manager()
registry = AgentRegistry(tenant_id=tenant_id, config_manager=config_manager)
deps = OrchestratorDeps()
orchestrator = OrchestratorAgent(deps=deps, registry=registry, config_manager=config_manager)

# Execute query that may fail
input_data = OrchestratorInput(query="test query", tenant_id=tenant_id)
result = await orchestrator._process_impl(input_data)

# On failure, orchestrator raises RuntimeError (no silent fallbacks)
assert result is not None

Best Practices

  1. Isolation: Each test should be independent
  2. Cleanup: Always cleanup test data after tests
  3. Timeouts: Set reasonable timeouts for integration tests
  4. Retries: Implement retry logic for flaky tests
  5. Logging: Enable detailed logging for debugging
  6. Metrics: Collect performance metrics during tests

Package-Level Integration Tests

Testing Foundation Layer

# Example: Verify SDK interfaces are properly implemented
from cogniverse_sdk.interfaces.backend import Backend
from cogniverse_vespa.backend import VespaBackend

# Verify Vespa implements SDK interface
assert issubclass(VespaBackend, Backend)

# Example: Verify foundation config is usable by core
from cogniverse_foundation.config.utils import create_default_config_manager
from cogniverse_core.agents.base import AgentDeps, AgentInput, AgentOutput
from cogniverse_foundation.telemetry.config import TelemetryConfig

config_manager = create_default_config_manager()

# AgentDeps carries infrastructure config (no tenant_id — it's per-request)
# Concrete agents use extended Deps classes:
# OrchestratorDeps(AgentDeps) adds orchestrator-specific config
# SearchAgentDeps(AgentDeps) adds: backend_url, backend_port, etc.
deps = AgentDeps()  # No tenant_id at construction

Testing Core Layer

# Example: Verify core works with evaluation package
from cogniverse_agents.orchestrator_agent import OrchestratorAgent, OrchestratorDeps, OrchestratorInput
from cogniverse_core.registries.agent_registry import AgentRegistry
from cogniverse_evaluation.core.experiment_tracker import ExperimentTracker
from cogniverse_foundation.config.utils import create_default_config_manager

tenant_id = "test"
config_manager = create_default_config_manager()
registry = AgentRegistry(tenant_id=tenant_id, config_manager=config_manager)
agent = OrchestratorAgent(deps=OrchestratorDeps(), registry=registry, config_manager=config_manager)

# Create experiment tracker for evaluation
# Note: tenant_id is REQUIRED (no default) — experiment_project_name defaults to "experiments"
tracker = ExperimentTracker(
    experiment_project_name="test_exp",
    tenant_id=tenant_id
)

# Execute orchestration query (tracked via telemetry)
result = await agent._process_impl(OrchestratorInput(query="test query", tenant_id=tenant_id))
assert result.workflow_id

# Example: Verify Phoenix telemetry plugin integration
from cogniverse_foundation.telemetry.registry import TelemetryRegistry
from cogniverse_telemetry_phoenix.provider import PhoenixProvider

# Verify plugin registration via entry points (classmethod `get`, tenant-scoped)
# Requires config with http_endpoint and grpc_endpoint
provider = TelemetryRegistry.get(
    name="phoenix",
    tenant_id=tenant_id,
    config={"http_endpoint": "http://localhost:6006", "grpc_endpoint": "http://localhost:4317"}
)
assert isinstance(provider, PhoenixProvider)

Testing Implementation Layer

# Example: Verify agents work with Vespa backend
from cogniverse_agents.search_agent import SearchAgent, SearchAgentDeps
from cogniverse_vespa.vespa_schema_manager import VespaSchemaManager
from cogniverse_foundation.config.utils import create_default_config_manager

tenant_id = "test"

# Setup tenant schemas
config_manager = create_default_config_manager()
schema_mgr = VespaSchemaManager(backend_endpoint="http://localhost", backend_port=8080)
tenant_schema = schema_mgr.get_tenant_schema_name(tenant_id, "video_colpali_smol500_mv_frame")

# Initialize agent with Vespa backend
search_deps = SearchAgentDeps(
    backend_url="http://localhost",
    backend_port=8080,
)
# profile is per-request, not at construction; tenant_id is per-request on search_by_text()
# schema_loader is REQUIRED (raises ValueError if None)
from pathlib import Path
from cogniverse_core.schemas.filesystem_loader import FilesystemSchemaLoader
schema_loader = FilesystemSchemaLoader(Path("configs/schemas"))
agent = SearchAgent(
    deps=search_deps,
    schema_loader=schema_loader,
    config_manager=config_manager
)

results = agent.search_by_text("test query", tenant_id=tenant_id, modality="video")
assert len(results) >= 0

# Example: Verify agents work with synthetic data
from cogniverse_agents.orchestrator_agent import OrchestratorAgent, OrchestratorDeps, OrchestratorInput
from cogniverse_core.registries.agent_registry import AgentRegistry
from cogniverse_synthetic.generators.routing import RoutingGenerator
from cogniverse_foundation.config.unified_config import (
    DSPyModuleConfig,
    OptimizerGenerationConfig,
)

# RoutingGenerator requires OptimizerGenerationConfig as REQUIRED parameter
# Configuration is REQUIRED - no fallbacks or defaults
# Create minimal config for testing (production would load from configs/config.json)
optimizer_config = OptimizerGenerationConfig(
    optimizer_type="routing",
    dspy_modules={
        "query_generator": DSPyModuleConfig(
            signature_class="cogniverse_synthetic.dspy_signatures.GenerateEntityQuery"
        )
    },
    profile_scoring_rules=[],
    agent_mappings=[],
)
# Production entity and routing callbacks are required; the generator never
# substitutes a local heuristic for either label.
generator = RoutingGenerator(
    entity_extractor=production_entity_extractor,
    routing_decider=production_routing_decider,
    optimizer_config=optimizer_config,
)

sampled_content = [
    {"video_id": "v1", "title": "Cooking tutorial", "description": "Learn to cook"},
    {"video_id": "v2", "title": "Science lecture", "description": "Physics explained"}
]
synthetic_data = await generator.generate(
    sampled_content=sampled_content,
    target_count=2,
    tenant_id=tenant_id,
)

# Test orchestrator with synthetic queries
orchestrator = OrchestratorAgent(
    deps=OrchestratorDeps(),
    registry=AgentRegistry(tenant_id=tenant_id, config_manager=config_manager),
    config_manager=config_manager,
)
for example in synthetic_data:
    result = await orchestrator._process_impl(
        OrchestratorInput(query=example.query, tenant_id=tenant_id)
    )
    assert result is not None
    assert result.workflow_id

Testing Application Layer

# Example: Verify runtime integrates all layers
from cogniverse_runtime.main import app
from fastapi.testclient import TestClient

# Dependency overrides (config_manager, schema_loader) are wired in the
# app's lifespan handler, so TestClient must be used as a context manager
# to trigger startup — a bare `TestClient(app)` skips lifespan and every
# route depending on those overrides returns 503.
with TestClient(app) as client:
    # Test end-to-end search request
    response = client.post(
        "/search/",
        json={"query": "cooking videos", "tenant_id": "test", "top_k": 10}
    )

    assert response.status_code == 200
    assert "results" in response.json()

# Example: experiment tracking against Phoenix
from cogniverse_evaluation.core.experiment_tracker import ExperimentTracker

# Create experiment tracker (tenant_id is REQUIRED — no default)
tracker = ExperimentTracker(
    experiment_project_name="dash_test",
    tenant_id="test"
)

# Phoenix persistent-data management (backup/restore/clean) is handled by
# the scripts/manage_phoenix_data.py CLI.

Next: For detailed instrumentation and telemetry patterns, see Phoenix Telemetry Integration