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 |
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(theRunnableConfig)Use
get_configurable_context(config)to safely extract theconfigurabledictBackward compatible: Existing factories with only
scenario_metadatacontinue to workThe
configparameter is detected via signature inspection—no registration changes neededget_configurable_contextreturns an empty dict if config isNoneor missingconfigurable
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
Quickstart Guide — Getting started with StuntDouble
LangGraph Approach — Per-invocation mocking
Call Recording — Verify tool calls in tests
Mock Format Reference — Mock data format