diff --git a/libs/core/tests/unit_tests/runnables/test_config.py b/libs/core/tests/unit_tests/runnables/test_config.py index dc7f1c5d0ca..3a8cb17d1da 100644 --- a/libs/core/tests/unit_tests/runnables/test_config.py +++ b/libs/core/tests/unit_tests/runnables/test_config.py @@ -161,3 +161,185 @@ async def test_run_in_executor() -> None: with pytest.raises(RuntimeError): await run_in_executor(None, raises_stop_iter) + + +def test_inherit_run_name_default_behavior() -> None: + """Test that by default, run_name is NOT inherited to child runs.""" + from langchain_core.runnables import RunnableLambda + from langchain_core.runnables.config import patch_config + + # Create a mock callback manager + class MockCallbackManager: + def get_child(self, tag: str = None) -> "MockCallbackManager": + return MockCallbackManager() + + # Test default behavior (inherit_run_name not set or False) + config: RunnableConfig = { + "run_name": "parent_run", + "callbacks": MockCallbackManager(), + } + + # When callbacks are replaced, run_name should be deleted by default + patched = patch_config(config, callbacks=MockCallbackManager()) + assert "run_name" not in patched, "run_name should be deleted by default" + + # Explicitly set inherit_run_name to False + config_explicit_false: RunnableConfig = { + "run_name": "parent_run", + "inherit_run_name": False, + "callbacks": MockCallbackManager(), + } + + patched_explicit = patch_config(config_explicit_false, callbacks=MockCallbackManager()) + assert "run_name" not in patched_explicit, "run_name should be deleted when inherit_run_name=False" + + +def test_inherit_run_name_enabled() -> None: + """Test that when inherit_run_name=True, run_name is preserved for child runs.""" + from langchain_core.runnables.config import patch_config + + # Create a mock callback manager + class MockCallbackManager: + def get_child(self, tag: str = None) -> "MockCallbackManager": + return MockCallbackManager() + + # Test with inherit_run_name=True + config: RunnableConfig = { + "run_name": "parent_run", + "inherit_run_name": True, + "callbacks": MockCallbackManager(), + } + + # When callbacks are replaced, run_name should be preserved + patched = patch_config(config, callbacks=MockCallbackManager()) + assert "run_name" in patched, "run_name should be preserved when inherit_run_name=True" + assert patched["run_name"] == "parent_run", "run_name value should be unchanged" + assert patched.get("inherit_run_name") is True, "inherit_run_name should be preserved" + + +def test_inherit_run_name_with_chain() -> None: + """Test inherit_run_name behavior in a chain of runnables.""" + from typing import List + from langchain_core.callbacks.base import BaseCallbackHandler + from langchain_core.runnables import RunnableLambda + + # Track run names through callbacks + captured_names: List[str] = [] + + class TestCallbackHandler(BaseCallbackHandler): + def on_chain_start(self, serialized: dict, inputs: Any, **kwargs: Any) -> None: + name = kwargs.get("name", "unnamed") + captured_names.append(name) + + # Create a simple chain + def identity(x: Any) -> Any: + return x + + def process(x: Any) -> str: + return f"processed: {x}" + + chain = RunnableLambda(identity) | RunnableLambda(process) + + # Test 1: Default behavior (run_name not inherited) + captured_names.clear() + config_default: RunnableConfig = { + "run_name": "custom_chain_run", + "callbacks": [TestCallbackHandler()], + } + result = chain.invoke("test", config=config_default) + assert result == "processed: test" + # First run should have custom name, child runs should have default names + assert captured_names[0] == "custom_chain_run", "Root run should have custom name" + # Child runs should NOT have the custom name (they'll have their default names) + for i in range(1, len(captured_names)): + assert captured_names[i] != "custom_chain_run", f"Child run {i} should not inherit run_name by default" + + # Test 2: With inherit_run_name=True + captured_names.clear() + config_inherit: RunnableConfig = { + "run_name": "inherited_chain_run", + "inherit_run_name": True, + "callbacks": [TestCallbackHandler()], + } + result = chain.invoke("test", config=config_inherit) + assert result == "processed: test" + # All runs should have the same custom name when inherit_run_name=True + assert captured_names[0] == "inherited_chain_run", "Root run should have custom name" + # With inherit_run_name=True, child runs should also have the custom name + for i in range(1, len(captured_names)): + assert captured_names[i] == "inherited_chain_run", f"Child run {i} should inherit run_name when inherit_run_name=True" + + +def test_inherit_run_name_with_override() -> None: + """Test that per-step with_config can override inherited run_name.""" + from typing import List + from langchain_core.callbacks.base import BaseCallbackHandler + from langchain_core.runnables import RunnableLambda + + # Track run names through callbacks + captured_names: List[str] = [] + + class TestCallbackHandler(BaseCallbackHandler): + def on_chain_start(self, serialized: dict, inputs: Any, **kwargs: Any) -> None: + name = kwargs.get("name", "unnamed") + captured_names.append(name) + + # Create a chain where one step has its own run_name + def identity(x: Any) -> Any: + return x + + def process(x: Any) -> str: + return f"processed: {x}" + + # Middle step has explicit run_name override + chain = ( + RunnableLambda(identity) | + RunnableLambda(process).with_config(run_name="step_override") | + RunnableLambda(identity) + ) + + captured_names.clear() + config: RunnableConfig = { + "run_name": "global_run", + "inherit_run_name": True, + "callbacks": [TestCallbackHandler()], + } + + result = chain.invoke("test", config=config) + assert result == "processed: test" + + # Check that the override is present + assert "step_override" in captured_names, "Step with explicit run_name should override inherited value" + assert "global_run" in captured_names, "Global run_name should still be present for other steps" + + +def test_inherit_run_name_merge_configs() -> None: + """Test that inherit_run_name is properly handled in merge_configs.""" + from langchain_core.runnables.config import merge_configs + + # Test merging with inherit_run_name + base: RunnableConfig = { + "run_name": "base_run", + "inherit_run_name": False, + } + + override: RunnableConfig = { + "inherit_run_name": True, + } + + merged = merge_configs(base, override) + assert merged.get("inherit_run_name") is True, "inherit_run_name should be overridden" + assert merged.get("run_name") == "base_run", "run_name should be preserved from base" + + # Test that inherit_run_name passes through ensure_config + from langchain_core.runnables.config import ensure_config + + config_with_inherit: RunnableConfig = { + "run_name": "test_run", + "inherit_run_name": True, + } + + ensured = ensure_config(config_with_inherit) + assert ensured.get("inherit_run_name") is True, "inherit_run_name should pass through ensure_config" + assert ensured.get("run_name") == "test_run", "run_name should be preserved" +