Skip to content

Commit 5898959

Browse files
authored
chore: add LangGraph node_metrics isolation regression test (#176)
1 parent ffc40a7 commit 5898959

1 file changed

Lines changed: 70 additions & 0 deletions

File tree

packages/ai-providers/server-ai-langchain/tests/test_langgraph_agent_graph_runner.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for LangGraphAgentGraphRunner and LangChainRunnerFactory.create_agent_graph()."""
22

3+
from uuid import uuid4
34
from unittest.mock import AsyncMock, MagicMock, patch
45

56
import pytest
@@ -156,3 +157,72 @@ async def test_langgraph_runner_run_success():
156157
tracker.track_path.assert_not_called()
157158
tracker.track_invocation_success.assert_not_called()
158159
tracker.track_duration.assert_not_called()
160+
161+
162+
@pytest.mark.asyncio
163+
async def test_langgraph_runner_run_resets_node_metrics_between_runs():
164+
"""Successive runs do not leak stale node metrics from a previous run.
165+
166+
Mirrors ``test_openai_agent_graph_runner_run_resets_node_metrics_between_runs``
167+
in the OpenAI provider tests. Each ``run()`` invocation must produce its
168+
own fresh ``node_metrics`` rather than a union of all prior runs' metrics.
169+
170+
Strategy: bypass ``_build_graph()`` by pre-populating ``_compiled`` and
171+
``_node_keys`` on the runner. The mock compiled graph's ``ainvoke`` is a
172+
side-effect coroutine that fires callbacks on the handler passed in via
173+
``config['callbacks']`` — the same handler the real LangGraph executor
174+
would invoke. Each call fires events for only ``root-agent`` so we can
175+
assert the second result's ``node_metrics`` reflects only the second run.
176+
"""
177+
graph = _make_graph()
178+
179+
mock_message = MagicMock()
180+
mock_message.content = "answer"
181+
mock_message.usage_metadata = None
182+
mock_message.response_metadata = None
183+
184+
async def fire_callbacks(_payload, *, config):
185+
handler = config['callbacks'][0]
186+
# If state leaked across runs, the handler passed in here on the
187+
# second call would already contain entries from the first run before
188+
# any callback fires. We assert below that this is not the case.
189+
run_id = uuid4()
190+
handler.on_chain_start({}, {}, run_id=run_id, name='root-agent')
191+
handler.on_chain_end({}, run_id=run_id)
192+
return {'messages': [mock_message]}
193+
194+
mock_compiled = MagicMock()
195+
mock_compiled.ainvoke = AsyncMock(side_effect=fire_callbacks)
196+
197+
mock_human_message = MagicMock()
198+
mock_lc_core_messages = MagicMock()
199+
mock_lc_core_messages.HumanMessage = MagicMock(return_value=mock_human_message)
200+
201+
runner = LangGraphAgentGraphRunner(graph, {})
202+
# Bypass _build_graph(): provide a pre-compiled graph and the node keys
203+
# that the callback handler would otherwise be initialised with.
204+
runner._compiled = mock_compiled
205+
runner._node_keys = {'root-agent'}
206+
runner._fn_name_to_config_key = {}
207+
208+
with patch.dict('sys.modules', {
209+
'langchain_core': MagicMock(),
210+
'langchain_core.messages': mock_lc_core_messages,
211+
}):
212+
first = await runner.run("attempt 1")
213+
assert first.metrics.success is True
214+
assert 'root-agent' in first.metrics.node_metrics
215+
first_metrics = first.metrics.node_metrics['root-agent']
216+
217+
second = await runner.run("attempt 2")
218+
219+
assert second.metrics.success is True
220+
assert 'root-agent' in second.metrics.node_metrics
221+
# The second run's per-node metrics must be a fresh object, not the
222+
# accumulated state from the first run. If the runner leaked the
223+
# callback handler (or its state dict) across invocations, the second
224+
# run would return the same LDAIMetrics instance with cumulative values.
225+
assert second.metrics.node_metrics['root-agent'] is not first_metrics
226+
# Path and node_metrics keys reflect only the second invocation.
227+
assert second.metrics.path == ['root-agent']
228+
assert set(second.metrics.node_metrics.keys()) == {'root-agent'}

0 commit comments

Comments
 (0)