> ## Documentation Index
> Fetch the complete documentation index at: https://docs.maximem.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> Patterns for testing application code that depends on Synap: unit tests with mocks, fixtures, and integration tests against a real Instance.

Testing code that calls the Synap SDK falls into three patterns. Pick by how realistic you need the test to be.

## 1. Unit tests with mocks

Use this for testing **your business logic** that happens around Synap calls: prompt assembly, decision logic, response handling. Mock the SDK so the test is fast and deterministic and doesn't need network.

```python theme={null}
# tests/test_chat.py
import pytest
from unittest.mock import AsyncMock, MagicMock
from maximem_synap import ContextResponse, Fact, ResponseMetadata
from datetime import datetime, timezone

from myapp.chat import handle_turn       # your code under test


def make_fake_context(facts: list[str]) -> ContextResponse:
    """Build a realistic ContextResponse without hitting the network."""
    return ContextResponse(
        facts=[
            Fact(
                id=f"fact_{i}",
                content=content,
                confidence=0.9,
                source="test",
                extracted_at=datetime.now(timezone.utc),
            )
            for i, content in enumerate(facts)
        ],
        metadata=ResponseMetadata(
            correlation_id="test-corr-id",
            ttl_seconds=300,
            source="cloud",
            retrieved_at=datetime.now(timezone.utc),
        ),
    )


@pytest.fixture
def fake_sdk():
    sdk = MagicMock()
    sdk.conversation.context.fetch = AsyncMock(return_value=make_fake_context([]))
    sdk.memories.create = AsyncMock(return_value=MagicMock(ingestion_id="ing_test"))
    return sdk


async def test_handle_turn_uses_facts_in_prompt(fake_sdk):
    fake_sdk.conversation.context.fetch.return_value = make_fake_context(
        ["User prefers dark mode", "User is on the Pro plan"]
    )

    reply = await handle_turn(
        sdk=fake_sdk,
        user_id="user_test",
        customer_id="cust_test",
        conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
        message="What plan am I on?",
    )

    # Assert the system prompt mentioned both retrieved facts
    call_args = fake_sdk.openai_client.chat.completions.create.call_args  # if you injected it
    system = call_args.kwargs["messages"][0]["content"]
    assert "dark mode" in system
    assert "Pro plan" in system


async def test_handle_turn_ingests_the_turn(fake_sdk):
    await handle_turn(sdk=fake_sdk, user_id="u", customer_id="c",
                      conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
                      message="Hi")

    fake_sdk.memories.create.assert_called_once()
    kwargs = fake_sdk.memories.create.call_args.kwargs
    assert kwargs["user_id"] == "u"
    assert kwargs["customer_id"] == "c"
    assert kwargs["document_type"] == "ai-chat-conversation"
```

**Why use the real Pydantic models when mocking**

Constructing a real `ContextResponse` instead of `MagicMock` catches field-name typos at test-write time. If you later upgrade the SDK and a field is removed, the test fails loudly instead of silently passing on a mock that "accepts everything."

## 2. FastAPI integration tests with dependency overrides

If you wired the SDK via `Depends(get_sdk)` (the recommended pattern in [Setup → Integration](/setup/detailed-integration)), FastAPI's `app.dependency_overrides` swaps it out per test.

```python theme={null}
from fastapi.testclient import TestClient
from myapp.main import app, get_sdk

def test_chat_endpoint_with_mock_sdk():
    fake_sdk = MagicMock()
    fake_sdk.conversation.context.fetch = AsyncMock(return_value=make_fake_context([]))
    fake_sdk.memories.create = AsyncMock()

    app.dependency_overrides[get_sdk] = lambda: fake_sdk
    try:
        client = TestClient(app)
        response = client.post("/chat", json={
            "message": "hello",
            "user_id": "u",
            "customer_id": "c",
            "conversation_id": "3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
        })
        assert response.status_code == 200
    finally:
        app.dependency_overrides.clear()
```

## 3. End-to-end against a real test Instance

For the highest-fidelity tests (pre-release smoke tests, contract tests against new SDK versions) run against a dedicated test Instance in Synap Cloud.

```python theme={null}
import asyncio
import os
import uuid
import pytest
from maximem_synap import MaximemSynapSDK

pytestmark = pytest.mark.integration   # opt-in marker so unit suite stays fast


@pytest.fixture(scope="session")
async def real_sdk():
    """One real SDK per test session, using a dedicated test instance."""
    sdk = MaximemSynapSDK(api_key=os.environ["SYNAP_TEST_API_KEY"])
    await sdk.initialize()
    yield sdk
    await sdk.shutdown()


# Note: the async session fixture above requires `pytest-asyncio` configured with
# `asyncio_mode = "auto"`, or switch the decorator to `@pytest_asyncio.fixture(scope="session")`.
# Under strict `pytest-asyncio` mode, plain `@pytest.fixture` async fixtures don't execute.


@pytest.fixture
def test_ids():
    """Per-test isolated user/customer/conversation IDs so tests don't interfere."""
    return {
        "user_id": f"test_user_{uuid.uuid4()}",
        "customer_id": f"test_cust_{uuid.uuid4()}",
        "conversation_id": str(uuid.uuid4()),
    }


async def test_ingest_then_retrieve_roundtrip(real_sdk, test_ids):
    """Smoke test: ingestion → retrieval works end-to-end."""
    await real_sdk.memories.create(
        document="User: I prefer dark mode.\nAssistant: Noted!",
        document_type="ai-chat-conversation",
        **test_ids,
    )

    # Wait for async processing
    await asyncio.sleep(5)

    ctx = await real_sdk.conversation.context.fetch(
        conversation_id=test_ids["conversation_id"],
        search_query=["dark mode"],
        user_id=test_ids["user_id"],
        customer_id=test_ids["customer_id"],
    )

    assert any("dark mode" in p.content.lower() for p in ctx.preferences)
```

**Use `_force_new=True` in tests to bypass the SDK singleton**

The SDK keeps one live instance per API key. In tests where you want a fresh instance per test, opt out of that:

```python theme={null}
sdk = MaximemSynapSDK(api_key="...", _force_new=True)
```

An SDK built this way is never registered as the singleton for its key, so tests can create and discard them freely: shutting one down won't disturb an SDK your application already holds for the same key.

For the same reason, nothing else will ever close it for you: `await sdk.shutdown()` in your fixture teardown, or each test leaks its own transports, streaming channel and cache handles for the rest of the run.

Don't reach for it in application code. You don't need it to run several tenants in one process, because different API keys already give you separate SDKs (see [Initialization](/sdk/initialization#running-multiple-api-keys-in-one-process)). Every extra SDK pays for its own connections, streaming channel and cache handles.

**Use unique IDs per test**

Synap memories persist. Two tests that both ingest `user_id="alice"` will see each other's data, and your assertions will be flaky. Always derive `user_id`, `customer_id`, `conversation_id` from `uuid.uuid4()` inside each test.

## 4. Snapshot testing of prompts

If your application generates LLM system prompts that incorporate Synap context, snapshot-test the rendered prompt to catch unintended drift.

```python theme={null}
from syrupy import snapshot

async def test_prompt_renders_consistently(fake_sdk, snapshot):
    fake_sdk.conversation.context.fetch.return_value = make_fake_context([
        "User prefers dark mode",
        "User is on Pro plan",
    ])
    prompt = await build_prompt(fake_sdk, user_id="u", customer_id="c",
                                conversation_id="3f6b1a2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
                                message="What plan am I on?")
    assert prompt == snapshot
```

Re-snapshot intentionally when you change prompt format; fail loudly when you change it by accident.

## What to skip in tests

* **Don't mock the SDK's internal transport; mock its public methods.** Mock the SDK methods (`memories.create`, `context.fetch`); those are your seams. Mocking the transport couples your tests to internal SDK structure that will change.
* **Don't snapshot `ContextResponse` objects directly.** They include timestamps and correlation IDs that change every run. Snapshot the *prompt string* you assemble from them.
* **Don't share `user_id`s across tests.** Synap memories are real and persist; cross-test pollution will bite.
