mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 06:53:14 +00:00
* fix: defensive copy in get_reviewer_agent and get_chat_agent [closes #1584] Factory functions were mutating the caller's RunnableConfig in-place via config['recursion_limit'] = DEFAULT_RECURSION_LIMIT. Add copy.deepcopy(config) at the top of each factory and switch the recursion_limit write to setdefault so a caller-supplied ceiling is respected. * fix: preserve runtime config object identities Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> --------- Co-authored-by: Fleet Agent <fleet-agent@langchain.dev> Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> (cherry picked from commit c34e04f44da7ec7638cdafc71e0bb40e77070efd) Co-authored-by: Jacob Albert <122248719+jacobalbert3@users.noreply.github.com>
122 lines
4.3 KiB
Python
122 lines
4.3 KiB
Python
"""Tests that get_reviewer_agent and get_chat_agent do not mutate the caller's config."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from langgraph.graph.state import RunnableConfig
|
|
|
|
|
|
def _make_config(recursion_limit: int = 25) -> RunnableConfig:
|
|
return {
|
|
"configurable": {"thread_id": None},
|
|
"recursion_limit": recursion_limit,
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_reviewer_agent_does_not_mutate_caller_config() -> None:
|
|
"""get_reviewer_agent must not overwrite the caller's recursion_limit."""
|
|
from agent import reviewer
|
|
|
|
config = _make_config(recursion_limit=25)
|
|
original_limit = config["recursion_limit"]
|
|
|
|
fake_pregel = MagicMock()
|
|
fake_pregel.with_config = MagicMock(return_value=fake_pregel)
|
|
|
|
with patch("agent.reviewer.create_deep_agent", return_value=fake_pregel):
|
|
await reviewer.get_reviewer_agent(config)
|
|
|
|
assert config["recursion_limit"] == original_limit, (
|
|
f"get_reviewer_agent mutated caller's recursion_limit: "
|
|
f"expected {original_limit}, got {config['recursion_limit']}"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_reviewer_agent_applies_default_when_limit_unset() -> None:
|
|
"""get_reviewer_agent should apply DEFAULT_RECURSION_LIMIT when the caller didn't set one."""
|
|
from agent import reviewer
|
|
|
|
config: RunnableConfig = {"configurable": {"thread_id": None}}
|
|
|
|
fake_pregel = MagicMock()
|
|
fake_pregel.with_config = MagicMock(return_value=fake_pregel)
|
|
|
|
with patch("agent.reviewer.create_deep_agent", return_value=fake_pregel):
|
|
await reviewer.get_reviewer_agent(config)
|
|
|
|
assert "recursion_limit" not in config
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_chat_agent_does_not_mutate_caller_config() -> None:
|
|
"""get_chat_agent must not overwrite the caller's recursion_limit."""
|
|
from agent import chat
|
|
|
|
config = _make_config(recursion_limit=50)
|
|
original_limit = config["recursion_limit"]
|
|
|
|
fake_pregel = MagicMock()
|
|
fake_pregel.with_config = MagicMock(return_value=fake_pregel)
|
|
|
|
with patch("agent.chat.create_deep_agent", return_value=fake_pregel):
|
|
await chat.get_chat_agent(config)
|
|
|
|
assert config["recursion_limit"] == original_limit, (
|
|
f"get_chat_agent mutated caller's recursion_limit: "
|
|
f"expected {original_limit}, got {config['recursion_limit']}"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_chat_agent_applies_default_when_limit_unset() -> None:
|
|
"""get_chat_agent should apply DEFAULT_RECURSION_LIMIT when the caller didn't set one."""
|
|
from agent import chat
|
|
|
|
config: RunnableConfig = {"configurable": {"thread_id": None}}
|
|
|
|
fake_pregel = MagicMock()
|
|
fake_pregel.with_config = MagicMock(return_value=fake_pregel)
|
|
|
|
with patch("agent.chat.create_deep_agent", return_value=fake_pregel):
|
|
await chat.get_chat_agent(config)
|
|
|
|
assert "recursion_limit" not in config
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("module_name", "factory_name"),
|
|
[("agent.reviewer", "get_reviewer_agent"), ("agent.chat", "get_chat_agent")],
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_factory_copies_config_dicts_but_preserves_runtime_objects(
|
|
module_name: str, factory_name: str
|
|
) -> None:
|
|
"""Factory config isolation must preserve callback and configurable value identities."""
|
|
module = __import__(module_name, fromlist=[factory_name])
|
|
factory = getattr(module, factory_name)
|
|
callback = object()
|
|
configurable_value = object()
|
|
callbacks = [callback]
|
|
config: RunnableConfig = {
|
|
"configurable": {"thread_id": None, "custom_key": configurable_value},
|
|
"callbacks": callbacks,
|
|
}
|
|
|
|
fake_pregel = MagicMock()
|
|
fake_pregel.with_config = MagicMock(return_value=fake_pregel)
|
|
|
|
with patch(f"{module_name}.create_deep_agent", return_value=fake_pregel):
|
|
await factory(config)
|
|
|
|
bound_config = fake_pregel.with_config.call_args.args[0]
|
|
assert bound_config is not config
|
|
assert bound_config["configurable"] is not config["configurable"]
|
|
assert bound_config["configurable"]["custom_key"] is configurable_value
|
|
assert bound_config["callbacks"] is callbacks
|
|
assert bound_config["callbacks"][0] is callback
|
|
assert "recursion_limit" not in config
|
|
assert config["configurable"] == {"thread_id": None, "custom_key": configurable_value}
|