Context-Aware Mocks

Access runtime context (user identity, request headers, tenant info) in your mock factories. This is especially useful for no-argument tools that need to return user-specific data.


Overview

Standard mock factories receive only scenario_metadata. Context-aware mock factories also receive the full RunnableConfig, allowing access to runtime context like authentication headers, user IDs, and tenant information.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Context-Aware Mock Flow                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   Standard Mock Factory:                                                    │
│   ──────────────────────                                                    │
│   mock_fn(scenario_metadata) → callable                                    │
│                                                                             │
│   Context-Aware Mock Factory:                                               │
│   ───────────────────────────                                               │
│   mock_fn(scenario_metadata, config) → callable                            │
│                         │                                                   │
│                         ▼                                                   │
│              get_configurable_context(config)                               │
│                         │                                                   │
│                         ▼                                                   │
│              {"agent_context": {"user_id": "U123", ...}}                   │
│                                                                             │
│   Detection: Automatic via signature inspection                            │
│   Backward compatible: Existing 1-arg factories still work                 │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

When to Use

Scenario

Recommendation

No-argument tools that need user context

✅ Use context-aware mocks

Mocks that vary based on request headers

✅ Use context-aware mocks

Multi-tenant testing scenarios

✅ Use context-aware mocks

Simple static mocks

Standard factory is sufficient

Mocks that only use scenario_metadata

Standard factory is sufficient


Quick Start

from stuntdouble import (
    MockToolsRegistry,
    create_mockable_tool_wrapper,
    get_configurable_context,
    inject_scenario_metadata,
)
from langgraph.prebuilt import ToolNode

registry = MockToolsRegistry()

# Context-aware mock factory: accepts BOTH scenario_metadata AND config
def user_context_mock(scenario_metadata: dict, config: dict = None):
    """Mock factory that extracts user context from RunnableConfig."""
    ctx = get_configurable_context(config)

    # Your application-specific extraction logic
    agent_context = ctx.get("agent_context", {})
    auth_header = agent_context.get("auth_header", {})
    user_id = auth_header.get("user_id", "unknown")
    org_id = auth_header.get("org_id", "unknown")

    # Return the mock callable
    return lambda: {"user_id": user_id, "org_id": org_id}

registry.register("get_current_user", mock_fn=user_context_mock)

# Build graph
wrapper = create_mockable_tool_wrapper(registry)
tool_node = ToolNode(tools, awrap_tool_call=wrapper)

When invoked, the RunnableConfig is automatically passed to your factory:

config = {
    "configurable": {
        "scenario_metadata": {"mocks": {}},
        "agent_context": {
            "auth_header": {
                "user_id": "USER-123",
                "org_id": "ORG-456"
            }
        }
    }
}

result = await graph.ainvoke(
    {"messages": [HumanMessage("Who am I?")]},
    config=config
)
# → get_current_user returns {"user_id": "USER-123", "org_id": "ORG-456"}

How It Works

Signature Inspection

StuntDouble detects context-aware factories automatically by inspecting the function signature:

# Standard factory (1 parameter) — receives scenario_metadata only
def standard_mock(scenario_metadata: dict):
    return lambda **kw: {"data": "static"}

# Context-aware factory (2 parameters) — receives both
def context_mock(scenario_metadata: dict, config: dict = None):
    ctx = get_configurable_context(config)
    return lambda **kw: {"user": ctx.get("user_id")}

No registration changes needed. The same registry.register() call works for both:

registry.register("tool_a", mock_fn=standard_mock)  # Works
registry.register("tool_b", mock_fn=context_mock)    # Also works

The get_configurable_context Helper

get_configurable_context(config) safely extracts the configurable dict from a RunnableConfig:

from stuntdouble import get_configurable_context

# Normal config
config = {"configurable": {"agent_context": {"user": "Alice"}}}
ctx = get_configurable_context(config)
# → {"agent_context": {"user": "Alice"}}

# None config (safe)
ctx = get_configurable_context(None)
# → {}

# Missing configurable key (safe)
ctx = get_configurable_context({})
# → {}

Examples

Example 1: User Identity Mock

A common pattern for tools that return the current user’s profile:

def current_user_mock(scenario_metadata: dict, config: dict = None):
    ctx = get_configurable_context(config)
    agent_context = ctx.get("agent_context", {})
    auth_header = agent_context.get("auth_header", {})

    user_id = auth_header.get("user_id", "test-user")
    org_id = auth_header.get("org_id", "test-org")

    return lambda: {
        "user_id": user_id,
        "realm_id": realm_id,
        "display_name": f"Test User ({user_id})"
    }

registry.register("get_current_user", mock_fn=current_user_mock)

Example 2: Tenant-Specific Data

Return different data based on the tenant/realm:

def tenant_mock(scenario_metadata: dict, config: dict = None):
    ctx = get_configurable_context(config)
    tenant_id = ctx.get("agent_context", {}).get("tenant_id", "default")

    tenant_configs = {
        "tenant-a": {"plan": "enterprise", "max_users": 1000},
        "tenant-b": {"plan": "startup", "max_users": 10},
        "default": {"plan": "free", "max_users": 1},
    }

    data = tenant_configs.get(tenant_id, tenant_configs["default"])
    return lambda: data

registry.register("get_tenant_config", mock_fn=tenant_mock)

Example 3: Request-Specific Mock

Use request headers to vary mock behavior:

def locale_mock(scenario_metadata: dict, config: dict = None):
    ctx = get_configurable_context(config)
    locale = ctx.get("headers", {}).get("Accept-Language", "en-US")

    translations = {
        "en-US": {"greeting": "Hello", "currency": "USD"},
        "es-MX": {"greeting": "Hola", "currency": "MXN"},
        "fr-FR": {"greeting": "Bonjour", "currency": "EUR"},
    }

    data = translations.get(locale, translations["en-US"])
    return lambda: data

registry.register("get_locale_settings", mock_fn=locale_mock)

Example 4: Combined with scenario_metadata

Use both scenario_metadata and config together:

def combined_mock(scenario_metadata: dict, config: dict = None):
    """Use scenario_metadata for mock data and config for runtime context."""
    ctx = get_configurable_context(config)
    user_id = ctx.get("agent_context", {}).get("user_id", "unknown")

    # Get mock data from scenario_metadata
    mocks = scenario_metadata.get("mocks", {})
    customer_data = mocks.get("get_customer", [{}])[0].get("output", {})

    def mock_fn(customer_id: str) -> dict:
        return {
            **customer_data,
            "requested_by": user_id,  # From runtime config
            "customer_id": customer_id,  # From tool input
        }

    return mock_fn

registry.register("get_customer", mock_fn=combined_mock)

Testing with Context-Aware Mocks

pytest Example

import pytest
from stuntdouble import (
    MockToolsRegistry,
    create_mockable_tool_wrapper,
    get_configurable_context,
    inject_scenario_metadata,
)

@pytest.fixture
def user_config():
    """Create config with user context."""
    config = inject_scenario_metadata({}, {"mocks": {}})
    config["configurable"]["agent_context"] = {
        "auth_header": {
            "user_id": "TEST-USER-001",
            "org_id": "TEST-ORG-001"
        }
    }
    return config

@pytest.fixture
def registry_with_context_mocks():
    registry = MockToolsRegistry()

    def user_mock(scenario_metadata: dict, config: dict = None):
        ctx = get_configurable_context(config)
        user_id = ctx.get("agent_context", {}).get("auth_header", {}).get("user_id", "unknown")
        return lambda: {"user_id": user_id}

    registry.register("get_current_user", mock_fn=user_mock)
    return registry

async def test_context_aware_mock(registry_with_context_mocks, user_config):
    wrapper = create_mockable_tool_wrapper(registry_with_context_mocks)
    # ... build graph and invoke with user_config ...
    # get_current_user will return {"user_id": "TEST-USER-001"}

Multi-Tenant Test

@pytest.mark.parametrize("tenant_id,expected_plan", [
    ("tenant-a", "enterprise"),
    ("tenant-b", "startup"),
    ("unknown", "free"),
])
async def test_tenant_specific_behavior(tenant_id, expected_plan):
    config = inject_scenario_metadata({}, {"mocks": {}})
    config["configurable"]["agent_context"] = {"tenant_id": tenant_id}

    result = await graph.ainvoke(
        {"messages": [HumanMessage("What's my plan?")]},
        config=config
    )
    # Assert based on expected_plan

Key Points

  • Mock factories can accept an optional second parameter config (the RunnableConfig)

  • Use get_configurable_context(config) to safely extract the configurable dict

  • Backward compatible: Existing factories with only scenario_metadata continue to work

  • The config parameter is detected via signature inspection—no registration changes needed

  • get_configurable_context returns an empty dict if config is None or missing configurable


More Patterns

Mock Factory Using Both scenario_metadata and config

A mock can merge test-scenario data with runtime context. scenario_metadata carries test-scenario data (locale, feature flags), while config carries per-request runtime context (user ID, auth tokens).

from stuntdouble import get_configurable_context

def personalized_mock(scenario_metadata: dict, config: dict = None):
    """Mock that merges scenario data with runtime config."""
    ctx = get_configurable_context(config)
    user_id = ctx.get("agent_context", {}).get("user_id", "anonymous")
    locale = scenario_metadata.get("locale", "en-US")

    def fn(query: str) -> dict:
        return {
            "results": [{"title": f"Result for '{query}'", "locale": locale}],
            "requested_by": user_id,
            "cached": False,
        }
    return fn

registry.register("search", mock_fn=personalized_mock)

Using {{config.*}} Placeholders in Scenario Definitions

You can reference RunnableConfig values directly in JSON scenarios without writing Python. {{config.*}} pulls values from RunnableConfig.configurable at resolution time. Use | default(...) for optional fields.

{
  "scenario_id": "user_dashboard",
  "mocks": {
    "get_profile": [
      {
        "output": {
          "user_id": "{{config.user_id}}",
          "org_id": "{{config.org_id | default('unknown')}}",
          "last_login": "{{now}}",
          "dashboard_url": "/users/{{config.user_id}}/dashboard"
        }
      }
    ]
  }
}

Multi-Tenant Mock with Config-Driven Behavior

Test the same scenario across different tenant configurations. Same mock definition, different runtime context — this is powerful for testing multi-tenant agents where behavior varies by config.

from stuntdouble import MockToolsRegistry, create_mockable_tool_wrapper, inject_scenario_metadata

registry = MockToolsRegistry()

def tenant_db_mock(scenario_metadata: dict, config: dict = None):
    ctx = get_configurable_context(config)
    region = ctx.get("agent_context", {}).get("region", "us-east-1")

    return lambda table, query: {
        "results": [{"id": "1", "region": region}],
        "source": f"db-{region}",
    }

registry.register("query_database", mock_fn=tenant_db_mock)
wrapper = create_mockable_tool_wrapper(registry)

# Test US tenant
us_config = inject_scenario_metadata(
    {"configurable": {"agent_context": {"region": "us-east-1"}}},
    {"scenario_id": "multi-region-test"}
)

# Test EU tenant — same scenario, different config
eu_config = inject_scenario_metadata(
    {"configurable": {"agent_context": {"region": "eu-west-1"}}},
    {"scenario_id": "multi-region-test"}
)

See Also