> ## 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 cache**

The SDK caches one instance per process by default. In tests where you want a fresh instance per test:

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

Don't use this in production; it creates multiple instances competing for the same credential files and local cache.

**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.
