""" Async LangGraph Tests All asynchronous tests using .ainvoke() and .astream() """ import os import pytest from langchain_core.messages import HumanMessage from deepeval.integrations.langchain import CallbackHandler from tests.test_integrations.utils import ( assert_trace_json, generate_trace_json, is_generate_mode, ) pytestmark = pytest.mark.flaky(reruns=3, reruns_delay=2) # App imports from tests.test_integrations.test_langgraph.apps.langgraph_async_app import ( app as async_app, ) from tests.test_integrations.test_langgraph.apps.langgraph_streaming_app import ( async_app as streaming_async_app, ) from tests.test_integrations.test_langgraph.apps.langgraph_conditional_app import ( app as conditional_app, ) from tests.test_integrations.test_langgraph.apps.langgraph_parallel_tools_app import ( async_app as parallel_async_app, ) from tests.test_integrations.test_langgraph.apps.langgraph_multi_turn_app import ( get_async_app_with_memory, ) from tests.test_integrations.test_langgraph.apps.langgraph_next_span_app import ( ainvoke_with_next_llm_span, ) # ============================================================================= # CONFIGURATION # ============================================================================= _current_dir = os.path.dirname(os.path.abspath(__file__)) _schemas_dir = os.path.join(_current_dir, "schemas") def trace_test(schema_name: str): """ Decorator that switches between generate and assert mode based on GENERATE_SCHEMAS env var. Args: schema_name: Name of the schema file (without path) """ schema_path = os.path.join(_schemas_dir, schema_name) if is_generate_mode(): return generate_trace_json(schema_path) else: return assert_trace_json(schema_path) # ============================================================================= # ASYNC APP TESTS # ============================================================================= class TestAsyncApp: """Tests for async LangGraph agent invocation.""" @pytest.mark.asyncio @trace_test("langgraph_async_single_tool_schema.json") async def test_single_tool(self): """Test async invocation with a single tool call.""" callback = CallbackHandler( name="langgraph-async-single", tags=["langgraph", "async", "single-tool"], metadata={"test_type": "async_single"}, thread_id="async-single-123", user_id="async-user", ) result = await async_app.ainvoke( { "messages": [ HumanMessage( content=( "Use the search_database tool to look up 'Rust (programming language)'. " "Do not ask clarification questions." ) ) ] }, config={"callbacks": [callback]}, ) assert len(result["messages"]) > 0 @pytest.mark.asyncio @trace_test("langgraph_async_multiple_tools_schema.json") async def test_multiple_tools(self): """Test async invocation with multiple tool calls.""" callback = CallbackHandler( name="langgraph-async-multi", tags=["langgraph", "async", "multi-tool"], metadata={"test_type": "async_multi"}, thread_id="async-multi-123", user_id="async-user", ) result = await async_app.ainvoke( { "messages": [ HumanMessage( content=( "Use the search_database tool to look up 'Python (programming language)'. " "Then translate the result to Spanish using the translate tool. " "Do not ask clarification questions." ) ) ] }, config={"callbacks": [callback]}, ) assert len(result["messages"]) > 0 @pytest.mark.asyncio @trace_test("langgraph_async_no_tools_schema.json") async def test_no_tool_needed(self): """Test async invocation where no tool is needed.""" callback = CallbackHandler( name="langgraph-async-no-tools", tags=["langgraph", "async", "no-tools"], ) result = await async_app.ainvoke( {"messages": [HumanMessage(content="Hello, how are you?")]}, config={"callbacks": [callback]}, ) assert len(result["messages"]) > 0 # ============================================================================= # ASYNC STREAMING TESTS # ============================================================================= class TestAsyncStreamingApp: """Tests for async streaming LangGraph agent.""" @pytest.mark.asyncio @trace_test("langgraph_async_streaming_schema.json") async def test_async_streaming(self): """Test async streaming with tool calls.""" callback = CallbackHandler( name="langgraph-streaming-async", tags=["langgraph", "streaming", "async"], metadata={"test_type": "streaming_async"}, ) chunks = [] async for chunk in streaming_async_app.astream( { "messages": [ HumanMessage(content="What's the stock price of GOOGL?") ] }, config={"callbacks": [callback]}, ): chunks.append(chunk) assert len(chunks) > 0 @pytest.mark.asyncio @trace_test("langgraph_async_streaming_multi_schema.json") async def test_async_streaming_multiple_tools(self): """Test async streaming with multiple tool calls.""" callback = CallbackHandler( name="langgraph-streaming-async-multi", tags=["langgraph", "streaming", "async", "multi"], ) chunks = [] async for chunk in streaming_async_app.astream( { "messages": [ HumanMessage( content="Get the stock price and company info for AMZN" ) ] }, config={"callbacks": [callback]}, ): chunks.append(chunk) assert len(chunks) > 0 # ============================================================================= # ASYNC CONDITIONAL ROUTING TESTS # ============================================================================= class TestAsyncConditionalApp: """Tests for async conditional routing LangGraph agent.""" @pytest.mark.asyncio @trace_test("langgraph_async_conditional_schema.json") async def test_async_conditional_routing(self): """Test async conditional routing.""" callback = CallbackHandler( name="langgraph-conditional-async", tags=["langgraph", "conditional", "async"], ) result = await conditional_app.ainvoke( { "messages": [ HumanMessage( content=( "Use the research tool exactly once to research: space exploration. " "Do not ask clarification questions. " "After the tool returns, respond with a short 3-bullet summary and stop." ) ) ] }, config={"callbacks": [callback]}, ) assert len(result["messages"]) > 0 # ============================================================================= # ASYNC PARALLEL TOOLS TESTS # ============================================================================= class TestAsyncParallelToolsApp: """Tests for async parallel tool execution LangGraph agent.""" @pytest.mark.asyncio @trace_test("langgraph_async_parallel_schema.json") async def test_async_parallel_tools(self): """Test async parallel tool execution.""" callback = CallbackHandler( name="langgraph-parallel-async", tags=["langgraph", "parallel", "async"], ) result = await parallel_async_app.ainvoke( { "messages": [ HumanMessage( content=( "Do the following using tools (do not ask clarification questions):" "1) Call get_weather with location=Sydney, Australia. " "2) Call get_weather with location=Tokyo, Japan. " "3) Call search_news with topic=tech. " "Then return a short combined result." ) ) ] }, config={"callbacks": [callback]}, ) assert len(result["messages"]) > 0 @pytest.mark.asyncio @trace_test("langgraph_async_parallel_heavy_schema.json") async def test_async_heavy_parallel(self): """Test async with many parallel tool calls.""" callback = CallbackHandler( name="langgraph-parallel-async-heavy", tags=["langgraph", "parallel", "async", "heavy"], ) result = await parallel_async_app.ainvoke( { "messages": [ HumanMessage( content=( "Call exactly these tools with the exact parameters shown. " "Do NOT use any other tools.\n\n" "1. get_weather(city='Tokyo')\n" "2. get_weather(city='New York')\n" "3. get_weather(city='London')\n" "4. get_weather(city='Paris')\n" "5. get_weather(city='Sydney')\n" "6. get_stock_price(symbol='AAPL')\n" "7. get_stock_price(symbol='GOOGL')\n" "8. get_stock_price(symbol='MSFT')\n" "9. calculate(expression='1/0.92')\n" "10. calculate(expression='1/0.79')\n" "11. calculate(expression='0.15*378.90')\n\n" "After receiving all results, provide a brief summary." ) ) ] }, config={"callbacks": [callback]}, ) assert len(result["messages"]) > 0 # ============================================================================= # ASYNC MULTI-TURN TESTS # ============================================================================= class TestAsyncMultiTurnApp: """Tests for async multi-turn conversation LangGraph agent.""" @pytest.mark.asyncio @trace_test("langgraph_async_multi_turn_schema.json") async def test_async_multi_turn(self): """Test async multi-turn conversation.""" # Create fresh app instance to avoid state leakage between tests app = get_async_app_with_memory() thread_id = "async-shopping-001" # Turn 1 callback1 = CallbackHandler( name="langgraph-async-multi-1", tags=["langgraph", "async", "multi-turn"], thread_id=thread_id, ) result1 = await app.ainvoke( {"messages": [HumanMessage(content="Add 5 apples to cart")]}, config={ "callbacks": [callback1], "configurable": {"thread_id": thread_id}, }, ) assert len(result1["messages"]) > 0 # Turn 2 callback2 = CallbackHandler( name="langgraph-async-multi-2", tags=["langgraph", "async", "multi-turn"], thread_id=thread_id, ) result2 = await app.ainvoke( {"messages": [HumanMessage(content="Apply FREESHIP coupon")]}, config={ "callbacks": [callback2], "configurable": {"thread_id": thread_id}, }, ) assert len(result2["messages"]) > 0 # ============================================================================= # ASYNC NEXT-SPAN STAGING TESTS (next_llm_span) # ============================================================================= class TestAsyncNextSpanApp: """Async counterpart of ``test_sync.py::TestNextSpanApp``. The pending-slot ContextVar must propagate through LangGraph's asyncio task scheduling to the chat-model callback inside the agent node so ``on_chat_model_start`` can pop it from the same task that issued the LLM invocation.""" @pytest.mark.asyncio @trace_test("langgraph_async_next_llm_span_schema.json") async def test_async_next_llm_span_only(self): callback = CallbackHandler( name="langgraph-async-next-llm-span", tags=["langgraph", "async", "next-llm"], metadata={"test_type": "async_next_llm_span"}, thread_id="async-next-llm-span-123", user_id="async-test-user", ) result = await ainvoke_with_next_llm_span( { "messages": [ HumanMessage( content="What is 9 squared? Call the tool and reply with just the number." ) ] }, metric_collection="llm_quality_async_v1", metadata={"prompt_variant": "B", "purpose": "async_next_llm_only"}, config={"callbacks": [callback]}, ) assert len(result["messages"]) > 0