Testing & Local Development
Test agents and tools locally without making live model calls.
#Testing and local development
Keep most SDK-facing tests offline: test tool bodies as normal Python, inspect compiled tool metadata, and replace only the remote invocation boundary when testing application behavior.
#Test tool functions directly
Decorators preserve the original callable, so dependencies do not change how direct unit tests invoke it.
from maivn import Agent, depends_on_private_data
agent = Agent(name='support', api_key='test-key')
@depends_on_private_data(data_key='account', arg_name='account')
@agent.toolify(name='lookup_account')
def lookup_account(account: dict[str, object], question: str) -> str:
return f"{account['name']}: {question}"
def test_lookup_account() -> None:
assert lookup_account({'name': 'Ada'}, 'status') == 'Ada: status'#Inspect compiled tool metadata
Agent.compile_tools() returns the actual public tool metadata sent to the runtime. It is the deterministic seam for names, descriptions, schemas, dependency elision, and final-tool selection.
def test_private_dependency_is_not_model_visible() -> None:
compiled = agent.compile_tools()[0]
assert compiled.name == 'lookup_account'
assert compiled.input_schema == {
'type': 'object',
'properties': {'question': {'type': 'string'}},
'additionalProperties': False,
'required': ['question'],
}Call agent.validate_tool_configuration() to check duplicate registered tool names. The swarm validator additionally checks final-output ownership. list_tools() returns current registered metadata. The SDK does not expose a full serialized-request inspection method.
#Test application behavior with InvokeResponse
InvokeResponse is exported from maivn. Construct it with its current required canonical fields when replacing a remote call:
from unittest.mock import patch
from maivn import Agent, InvokeResponse
from maivn.messages import HumanMessage
from maivn_contracts.messages import Message
def summarize_ticket(agent: Agent, ticket: str) -> str:
return agent.invoke([HumanMessage(content=ticket)]).response.strip()
def test_summarize_ticket() -> None:
agent = Agent(name='support', api_key='test-key')
final_message = Message(
message_id='msg-test',
role='assistant',
content=' Customer wants a refund. ',
ts='2026-08-08T12:00:00Z',
)
fake = InvokeResponse(
final_message=final_message,
session_id='session-test',
root_event_id='event-root',
event_positions=[1],
response=' Customer wants a refund. ',
)
with patch.object(agent, 'invoke', return_value=fake):
result = summarize_ticket(agent, 'I want my money back')
assert result == 'Customer wants a refund.'This test asserts application output, not the mock's existence. Prefer an httpx.MockTransport integration test when request serialization or stream/tool-runtime behavior is the contract under test.
#Test stream consumers
agent.stream() yields maivn.StreamEvent. Construct the canonical envelope and optional compatibility projection your consumer reads:
from maivn import StreamEvent
events = [
StreamEvent(
position=1,
event_type='session_progress',
data={'phase': 'planning'},
compat_name='enrichment',
compat_payload={'phase': 'planning'},
),
StreamEvent(
position=2,
event_type='session_completed',
data={'response': 'Done.'},
compat_name='final',
compat_payload={'response': 'Done.'},
),
]
assert [event.name for event in events] == ['enrichment', 'final']Use maivn.events.RawSSEEvent only when testing a consumer that starts from raw name/payload pairs, and pass those through normalize_stream(...) when that matches production.
#Fast local checks
- Use a non-empty dummy key for construction-only tests; do not use a real credential.
- Keep live-provider tests explicitly opt-in and separate from unit tests.
- Assert
ValueError,TypeError, or Pydantic validation failures for deterministic authoring mistakes. - Catch
MaivnHTTPErrororMaivnSDKErroronly when those public exception contracts are the behavior under test.
See Tools, Errors, and Frontend Events.