Files
EvoScientist/tests/test_graph_gateway.py
T
dinos 690b903f85 test: standardize async tests on pytest-asyncio auto mode (#338)
* chore: add pytest-asyncio in auto mode

* test: migrate channel and stream tests to native async

Convert run_async() wrapper tests to plain 'async def test_*' under
pytest-asyncio auto mode. collect_events() in stream_v3_fakes becomes a
coroutine awaited at every call site.

* test: migrate command and model/middleware tests to native async

Convert run_async() wrappers (import, alias, and fixture forms) to plain
'async def test_*'. Multi-call tests merge onto one loop as sequential
awaits; none asserted on loop identity.

* test: migrate TUI, notifier, gateway, and session tests to native async

TUI/notifier/gateway files convert run_async wrappers to plain async
tests. test_sessions.py's unittest.TestCase classes move to
unittest.IsolatedAsyncioTestCase (pytest-asyncio does not await async
methods on plain TestCase; converting blindly would have made ~70 tests
silently vacuous). Its setUpClass keeps a one-shot asyncio.run() since
IsolatedAsyncioTestCase has no async class-level hook. TestLoadingWidget
in test_tui_widgets.py drops its TestCase base for the same reason.

* test: replace direct asyncio.run() calls with native async tests

Convert tests that called asyncio.run() (directly or via a local _run
helper) to plain 'async def test_*'; delete the local helpers.

* test: drop undeclared anyio markers and delete run_async helper

The @pytest.mark.anyio tests relied on anyio being a transitive dep of
httpx; auto-mode pytest-asyncio collects them natively. run_async() and
its fixture are unreferenced after the migration, so remove them —
pytest-asyncio's per-test loop teardown covers the pending-task
cancellation the helper existed for (verified: full suite runs with no
'Event loop is closed' errors or destroyed-task warnings).
2026-07-08 18:37:48 +00:00

1062 lines
32 KiB
Python

"""Tests for the graph/thread gateway abstraction."""
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from langchain_core.messages import AIMessage, HumanMessage
from EvoScientist.gateway import (
GraphTarget,
LangGraphServerGateway,
LangGraphServerThreadStore,
LocalGraphGateway,
RunRequest,
RuntimeGateways,
create_runtime_gateways,
)
from EvoScientist.gateway.server import _THREAD_SEARCH_LIMIT
from EvoScientist.stream import display as display_mod
from tests.fakes import (
FakeGraphGateway,
FakeLangGraphClient,
FakeLangGraphThreadsClient,
FakeLangGraphThreadStream,
FakeThreadStore,
)
async def test_local_gateway_streams_from_injected_streamer():
seen: dict[str, Any] = {}
async def _streamer(agent, message, thread_id, **kwargs):
seen.update(
{
"agent": agent,
"message": message,
"thread_id": thread_id,
"metadata": kwargs.get("metadata"),
"media": kwargs.get("media"),
}
)
yield {"type": "text", "content": "hi"}
yield {"type": "done", "response": "hi"}
agent = MagicMock()
gateway = LocalGraphGateway()
async def _collect():
request = RunRequest(
message="hello",
thread_id="t1",
metadata={"workspace_dir": "/tmp/ws"},
media=["plot.png"],
target=GraphTarget(local_graph=agent, workspace_dir="/tmp/ws"),
)
return [event async for event in gateway.stream_events(request)]
with patch("EvoScientist.stream.events.stream_agent_events", new=_streamer):
events = await _collect()
assert events == [
{"type": "text", "content": "hi"},
{"type": "done", "response": "hi"},
]
assert seen == {
"agent": agent,
"message": "hello",
"thread_id": "t1",
"metadata": {"workspace_dir": "/tmp/ws"},
"media": ["plot.png"],
}
async def test_local_graph_gateway_delegates_thread_operations():
thread_store = FakeThreadStore(
generated_thread_id="new12345",
threads=[{"thread_id": "abc12345"}],
resolved_thread_id="abc12345",
metadata={"workspace_dir": "/tmp/ws"},
messages=["message"],
exists=True,
deleted=True,
)
async def _run():
gateway = LocalGraphGateway(thread_store=thread_store)
resolution = await gateway.resolve_thread("abc")
return {
"created": await gateway.create_thread(),
"threads": await gateway.list_threads(
limit=3,
include_message_count=True,
),
"resolution": resolution,
"metadata": await gateway.get_thread_metadata("abc12345"),
"messages": await gateway.get_thread_messages("abc12345"),
"exists": await gateway.thread_exists("abc12345"),
"deleted": await gateway.delete_thread("abc12345"),
}
result = await _run()
assert result["created"] == "new12345"
assert result["threads"] == [{"thread_id": "abc12345"}]
assert result["resolution"].thread_id == "abc12345"
assert result["resolution"].matches == ()
assert result["resolution"].found
assert not result["resolution"].ambiguous
assert result["metadata"] == {"workspace_dir": "/tmp/ws"}
assert result["messages"] == ["message"]
assert result["exists"] is True
assert result["deleted"] is True
assert thread_store.calls == [
("resolve_thread_id_prefix", "abc"),
("generate_thread_id", None),
(
"list_threads",
{
"limit": 3,
"include_message_count": True,
"include_preview": False,
},
),
("get_thread_metadata", "abc12345"),
("get_thread_messages", "abc12345"),
("thread_exists", "abc12345"),
("delete_thread", "abc12345"),
]
async def test_local_graph_gateway_reads_state_values():
agent = MagicMock()
agent.aget_state = AsyncMock(
return_value=SimpleNamespace(values={"async_tasks": {"task-1": {}}})
)
gateway = LocalGraphGateway()
values = await gateway.get_state_values(GraphTarget(local_graph=agent), "abc12345")
assert values == {"async_tasks": {"task-1": {}}}
agent.aget_state.assert_awaited_once_with(
{"configurable": {"thread_id": "abc12345"}}
)
async def test_local_graph_gateway_updates_state_values():
agent = MagicMock()
agent.aupdate_state = AsyncMock()
gateway = LocalGraphGateway()
await gateway.update_state_values(
GraphTarget(local_graph=agent),
"abc12345",
{"_summarization_event": {"cutoff_index": 2}},
)
agent.aupdate_state.assert_awaited_once_with(
{"configurable": {"thread_id": "abc12345"}},
{"_summarization_event": {"cutoff_index": 2}},
as_node="model",
)
async def test_local_stream_events_delegates_aclose_to_inner():
cleanup_ran = False
async def _streamer(_agent, _message, _thread_id, **_kwargs):
nonlocal cleanup_ran
try:
while True:
yield {"type": "event"}
finally:
cleanup_ran = True
async def _run():
gateway = LocalGraphGateway()
stream = gateway.stream_events(
RunRequest(
message="hi",
thread_id="t1",
target=GraphTarget(local_graph=object()),
)
)
await stream.__anext__()
await stream.aclose()
assert cleanup_ran is True
with patch("EvoScientist.stream.events.stream_agent_events", new=_streamer):
await _run()
def test_run_streaming_can_consume_injected_gateway():
agent = MagicMock()
gateway = FakeGraphGateway(
events=[
{"type": "text", "content": "gateway-ok"},
{"type": "done", "response": "gateway-ok"},
]
)
with patch("EvoScientist.stream.display.Live"):
result = display_mod._run_streaming(
agent=agent,
message="hello",
thread_id="t1",
show_thinking=False,
interactive=True,
metadata={"workspace_dir": "/tmp/ws"},
gateway=gateway,
)
assert result == "gateway-ok"
assert gateway.requests == [
RunRequest(
message="hello",
thread_id="t1",
metadata={"workspace_dir": "/tmp/ws"},
target=GraphTarget(local_graph=agent, workspace_dir="/tmp/ws"),
)
]
async def test_resume_command_consumes_context_gateway():
from EvoScientist.commands.base import CommandContext
from EvoScientist.commands.implementation.session import ResumeCommand
ui = MagicMock()
ui.handle_session_resume = AsyncMock()
thread_store = FakeThreadStore(
resolved_thread_id="abc12345",
metadata={"workspace_dir": "/restored"},
)
ctx = CommandContext(
agent=None,
thread_id="current",
ui=ui,
workspace_dir="/old",
graph_gateway=FakeGraphGateway(thread_store=thread_store),
)
await ResumeCommand().execute(ctx, ["abc"])
assert ctx.thread_id == "abc12345"
assert ctx.workspace_dir == "/restored"
ui.handle_session_resume.assert_awaited_once_with("abc12345", "/restored")
def test_cmd_run_passes_local_graph_gateway(monkeypatch):
from EvoScientist.cli import interactive
thread_store = FakeThreadStore(generated_thread_id="generated-thread")
runtime_gateways = RuntimeGateways(
thread_store=thread_store,
graph_gateway=LocalGraphGateway(thread_store=thread_store),
)
seen: dict[str, Any] = {}
def _run_streaming(**kwargs):
seen.update(kwargs)
return "ok"
monkeypatch.setattr(interactive, "run_streaming", _run_streaming)
agent = MagicMock()
interactive.cmd_run(
agent,
"hello",
thread_id="generated-thread",
show_thinking=False,
workspace_dir="/tmp/ws",
model="test-model",
runtime_gateways=runtime_gateways,
)
assert seen["agent"] is agent
assert seen["thread_id"] == "generated-thread"
assert isinstance(seen["gateway"], LocalGraphGateway)
assert seen["gateway"].thread_store is thread_store
async def test_langgraph_server_thread_store_delegates_to_sdk_threads():
threads = FakeLangGraphThreadsClient(
threads=[
{
"thread_id": "abc12345",
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-02T00:00:00Z",
"metadata": {"graph_id": "EvoScientist", "workspace_dir": "/tmp/ws"},
},
{
"thread_id": "worker123",
"metadata": {"graph_id": "evomemory-turn-worker"},
},
],
states={
"abc12345": {
"values": {
"messages": [
{"role": "user", "content": "hello from server"},
{"role": "assistant", "content": "hi"},
]
}
}
},
)
client = FakeLangGraphClient(threads)
store = LangGraphServerThreadStore(
client=client,
)
async def _run():
return {
"created": await store.create_thread(
metadata={"model": "test-model"},
workspace_dir="/tmp/new-ws",
),
"threads": await store.list_threads(
include_message_count=True,
include_preview=True,
),
"resolution": await store.resolve_thread_id_prefix("abc"),
"metadata": await store.get_thread_metadata("abc12345"),
"messages": await store.get_thread_messages("abc12345"),
"exists": await store.thread_exists("abc12345"),
"deleted": await store.delete_thread("abc12345"),
}
result = await _run()
assert result["created"] == "server-thread"
assert len(threads.created) == 1
assert threads.created[0]["thread_id"] == "server-thread"
created_metadata = threads.created[0]["metadata"]
assert created_metadata["graph_id"] == "EvoScientist"
assert created_metadata["agent_name"] == "EvoScientist"
assert created_metadata["workspace_dir"] == "/tmp/new-ws"
assert created_metadata["model"] == "test-model"
assert isinstance(created_metadata["updated_at"], str)
assert result["threads"] == [
{
"thread_id": "abc12345",
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-02T00:00:00Z",
"workspace_dir": "/tmp/ws",
"model": None,
"metadata": {"graph_id": "EvoScientist", "workspace_dir": "/tmp/ws"},
"message_count": 2,
"preview": "hello from server",
},
{
"thread_id": "server-thread",
"created_at": None,
"updated_at": None,
"workspace_dir": "/tmp/new-ws",
"model": "test-model",
"metadata": created_metadata,
"message_count": 0,
"preview": "",
},
]
assert result["resolution"] == ("abc12345", [])
assert result["metadata"] == {
"graph_id": "EvoScientist",
"workspace_dir": "/tmp/ws",
}
assert [message.type for message in result["messages"]] == ["human", "ai"]
assert result["exists"] is True
assert result["deleted"] is True
assert threads.deleted == ["abc12345"]
async def test_langgraph_server_thread_store_limit_zero_pages_all_threads():
rows = [
{
"thread_id": f"thread-{index}",
"metadata": {"graph_id": "EvoScientist"},
}
for index in range(_THREAD_SEARCH_LIMIT + 1)
]
threads = FakeLangGraphThreadsClient(threads=rows)
store = LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
result = await store.list_threads(limit=0)
assert [row["thread_id"] for row in result] == [
f"thread-{index}" for index in range(_THREAD_SEARCH_LIMIT + 1)
]
assert [(search["limit"], search["offset"]) for search in threads.searches] == [
(_THREAD_SEARCH_LIMIT, 0),
(_THREAD_SEARCH_LIMIT, _THREAD_SEARCH_LIMIT),
]
async def test_langgraph_server_thread_store_positive_limit_uses_single_search():
threads = FakeLangGraphThreadsClient(
threads=[
{
"thread_id": f"thread-{index}",
"metadata": {"graph_id": "EvoScientist"},
}
for index in range(3)
]
)
store = LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
result = await store.list_threads(limit=2)
assert [row["thread_id"] for row in result] == ["thread-0", "thread-1"]
assert [(search["limit"], search["offset"]) for search in threads.searches] == [
(2, 0)
]
async def test_langgraph_server_thread_store_prefix_resolution_skips_exact_lookup():
threads = FakeLangGraphThreadsClient(
threads=[
{
"thread_id": "abc12345",
"metadata": {"graph_id": "EvoScientist"},
}
]
)
store = LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
result = await store.resolve_thread_id_prefix("abc")
assert result == ("abc12345", [])
assert threads.gets == []
assert len(threads.searches) == 1
async def test_langgraph_server_thread_store_prefix_resolution_pages_all_threads():
rows = [
{
"thread_id": f"thread-{index}",
"metadata": {"graph_id": "EvoScientist"},
}
for index in range(_THREAD_SEARCH_LIMIT)
]
rows.append(
{
"thread_id": "older-thread-match",
"metadata": {"graph_id": "EvoScientist"},
}
)
threads = FakeLangGraphThreadsClient(threads=rows)
store = LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
result = await store.resolve_thread_id_prefix("older-thread")
assert result == ("older-thread-match", [])
assert [(search["limit"], search["offset"]) for search in threads.searches] == [
(_THREAD_SEARCH_LIMIT, 0),
(_THREAD_SEARCH_LIMIT, _THREAD_SEARCH_LIMIT),
]
async def test_langgraph_server_thread_store_uuid_resolution_uses_exact_lookup():
thread_id = "019ed9e4-4253-7f62-b50f-f0470a4b3c9f"
threads = FakeLangGraphThreadsClient(
threads=[
{
"thread_id": thread_id,
"metadata": {"graph_id": "EvoScientist"},
}
]
)
store = LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
result = await store.resolve_thread_id_prefix(thread_id)
assert result == (thread_id, [])
assert threads.gets == [thread_id]
assert threads.searches == []
async def test_langgraph_server_thread_store_uuid_resolution_filters_graph_id():
thread_id = "019ed9e4-4253-7f62-b50f-f0470a4b3c9f"
threads = FakeLangGraphThreadsClient(
threads=[
{
"thread_id": thread_id,
"metadata": {"graph_id": "other-agent"},
}
]
)
store = LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
result = await store.resolve_thread_id_prefix(thread_id)
assert result == (None, [])
assert threads.gets == [thread_id]
assert [(search["limit"], search["offset"]) for search in threads.searches] == [
(_THREAD_SEARCH_LIMIT, 0)
]
async def test_langgraph_server_thread_store_clones_thread_with_metadata():
clone_metadata = {
"clone_purpose": "memory_extraction",
"source_thread_id": "source-thread",
}
threads = FakeLangGraphThreadsClient(
threads=[
{
"thread_id": "source-thread",
"metadata": {"graph_id": "writing-agent", "workspace_dir": "/tmp/ws"},
}
]
)
store = LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
cloned_thread_id = await store.clone_thread(
"source-thread", metadata=clone_metadata
)
assert cloned_thread_id == "source-thread-copy"
assert threads.copied == ["source-thread"]
assert threads.metadata_updates == [("source-thread-copy", clone_metadata)]
assert threads.threads[-1] == {
"thread_id": "source-thread-copy",
"metadata": {
"graph_id": "writing-agent",
"workspace_dir": "/tmp/ws",
"clone_purpose": "memory_extraction",
"source_thread_id": "source-thread",
},
}
async def test_langgraph_server_thread_store_rejects_copy_without_thread_id():
threads = FakeLangGraphThreadsClient(
threads=[{"thread_id": "source-thread", "metadata": {"graph_id": "agent"}}],
copy_response=None,
)
store = LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
async def _run():
await store.clone_thread("source-thread")
with pytest.raises(RuntimeError, match="did not return a cloned thread id"):
await _run()
async def test_langgraph_server_gateway_clones_thread():
threads = FakeLangGraphThreadsClient(
threads=[{"thread_id": "source-thread", "metadata": {"graph_id": "agent"}}]
)
gateway = LangGraphServerGateway(
LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
)
cloned_thread_id = await gateway.clone_thread(
"source-thread",
metadata={"clone_purpose": "manual"},
target=GraphTarget(graph_id="agent"),
)
assert cloned_thread_id == "source-thread-copy"
assert threads.metadata_updates == [
("source-thread-copy", {"clone_purpose": "manual"})
]
async def test_local_graph_gateway_clone_thread_is_explicitly_unsupported():
async def _run():
await LocalGraphGateway().clone_thread("source-thread")
with pytest.raises(NotImplementedError, match="does not support thread cloning"):
await _run()
def test_runtime_gateways_can_use_langgraph_server_backend():
threads = FakeLangGraphThreadsClient()
client = FakeLangGraphClient(threads)
runtime_gateways = create_runtime_gateways(
backend="langgraph_server",
langgraph_client=client,
)
gateway = runtime_gateways.graph_gateway
assert isinstance(runtime_gateways.thread_store, LangGraphServerThreadStore)
assert isinstance(gateway, LangGraphServerGateway)
assert gateway.thread_store is runtime_gateways.thread_store
async def test_langgraph_server_gateway_reads_state_values():
threads = FakeLangGraphThreadsClient(
threads=[{"thread_id": "abc12345", "metadata": {"graph_id": "EvoScientist"}}],
states={"abc12345": {"values": {"async_tasks": {"task-1": {}}}}},
)
gateway = LangGraphServerGateway(
LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
)
values = await gateway.get_state_values(GraphTarget(), "abc12345")
assert values == {"async_tasks": {"task-1": {}}}
async def test_langgraph_server_gateway_messages_apply_summarization_event():
threads = FakeLangGraphThreadsClient(
threads=[{"thread_id": "abc12345", "metadata": {"graph_id": "EvoScientist"}}],
states={
"abc12345": {
"values": {
"messages": [
HumanMessage(content="first"),
AIMessage(content="second"),
HumanMessage(content="third"),
],
"_summarization_event": {
"cutoff_index": 2,
"summary_message": AIMessage(content="summary"),
"file_path": None,
},
}
}
},
)
gateway = LangGraphServerGateway(
LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
)
messages = await gateway.get_thread_messages("abc12345")
assert len(messages) == 2
assert isinstance(messages[0], AIMessage)
assert messages[0].content == "summary"
assert isinstance(messages[1], HumanMessage)
assert messages[1].content == "third"
async def test_langgraph_server_gateway_updates_state_values():
threads = FakeLangGraphThreadsClient(
threads=[{"thread_id": "abc12345", "metadata": {"graph_id": "EvoScientist"}}],
)
gateway = LangGraphServerGateway(
LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
)
await gateway.update_state_values(
GraphTarget(),
"abc12345",
{"_summarization_event": {"cutoff_index": 2}},
)
assert threads.state_updates == [
("abc12345", {"_summarization_event": {"cutoff_index": 2}}, "model")
]
async def test_langgraph_server_gateway_streams_root_protocol_events():
stream = FakeLangGraphThreadStream(
"abc12345",
events=[
{
"method": "messages",
"params": {
"namespace": [],
"data": {
"event": "content-block-delta",
"delta": {"type": "text-delta", "text": "hello"},
},
},
},
{
"method": "messages",
"params": {
"namespace": [],
"data": {"event": "message-finish"},
},
},
],
)
threads = FakeLangGraphThreadsClient(
threads=[],
states={"abc12345": {"values": {}}},
streams={"abc12345": stream},
)
gateway = LangGraphServerGateway(
LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
)
async def _collect():
return [
event
async for event in gateway.stream_events(
RunRequest(
message="hi",
thread_id="abc12345",
metadata={"workspace_dir": "/tmp/ws"},
target=GraphTarget(graph_id="writing-agent"),
)
)
]
events = await _collect()
assert len(threads.created) == 1
assert threads.created[0]["thread_id"] == "abc12345"
created_metadata = threads.created[0]["metadata"]
assert created_metadata["graph_id"] == "writing-agent"
assert created_metadata["workspace_dir"] == "/tmp/ws"
assert isinstance(created_metadata["updated_at"], str)
assert len(threads.metadata_updates) == 1
update_thread_id, update_metadata = threads.metadata_updates[0]
assert update_thread_id == "abc12345"
assert update_metadata["graph_id"] == "writing-agent"
assert update_metadata["workspace_dir"] == "/tmp/ws"
assert isinstance(update_metadata["updated_at"], str)
assert threads.stream_calls == [("abc12345", "writing-agent")]
assert stream.run.starts == [
{
"input": {"messages": [{"role": "user", "content": "hi"}]},
"config": {"configurable": {"thread_id": "abc12345"}},
"metadata": {"workspace_dir": "/tmp/ws"},
}
]
assert events == [
{"type": "text", "content": "hello"},
{"type": "done", "content": "hello", "response": "hello"},
]
_OLD_AI = {"type": "ai", "content": "old", "id": "old-ai"}
_HUMAN = {"type": "human", "content": "hi", "id": "human-1"}
_NEW_AI = {"type": "ai", "content": "new", "id": "new-ai"}
def _value_snapshot(
messages: list[dict[str, object]],
*,
namespace: list[str] | None = None,
) -> dict[str, object]:
return {
"method": "values",
"params": {
"namespace": namespace or [],
"data": {"messages": messages},
},
}
def _root_text_delta(text: str) -> dict[str, object]:
return {
"method": "messages",
"params": {
"namespace": [],
"data": {
"event": "content-block-delta",
"delta": {"type": "text-delta", "text": text},
},
},
}
def _root_message_finish() -> dict[str, object]:
return {
"method": "messages",
"params": {"namespace": [], "data": {"event": "message-finish"}},
}
async def _collect_server_gateway_stream(
events: list[dict[str, object]],
*,
state_messages: list[dict[str, object]] | None = None,
) -> list[dict[str, Any]]:
stream = FakeLangGraphThreadStream("abc12345", events=events)
state_values: dict[str, object] = {}
if state_messages is not None:
state_values["messages"] = state_messages
threads = FakeLangGraphThreadsClient(
threads=[],
states={"abc12345": {"values": state_values}},
streams={"abc12345": stream},
)
gateway = LangGraphServerGateway(
LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
)
async def _collect():
return [
event
async for event in gateway.stream_events(
RunRequest(message="hi", thread_id="abc12345")
)
]
return await _collect()
async def test_langgraph_server_gateway_streams_value_message_snapshots():
events = await _collect_server_gateway_stream(
[
_value_snapshot([_OLD_AI, _HUMAN]),
_value_snapshot([_OLD_AI, _HUMAN, _NEW_AI]),
],
state_messages=[_OLD_AI],
)
assert events == [
{"type": "text", "content": "new"},
{"type": "done", "content": "new", "response": "new"},
]
async def test_langgraph_server_gateway_values_do_not_duplicate_message_stream():
events = await _collect_server_gateway_stream(
[
_root_text_delta("new"),
_root_message_finish(),
_value_snapshot([_OLD_AI, _HUMAN, _NEW_AI]),
],
state_messages=[_OLD_AI],
)
assert events == [
{"type": "text", "content": "new"},
{"type": "done", "content": "new", "response": "new"},
]
async def test_langgraph_server_gateway_ignores_non_root_value_messages():
events = await _collect_server_gateway_stream(
[
_value_snapshot(
[{"type": "ai", "content": "subagent text", "id": "subagent-ai"}],
namespace=["research:task-1"],
)
],
)
assert not any(event.get("type") == "text" for event in events)
assert events[-1] == {"type": "done", "content": "", "response": ""}
async def test_langgraph_server_gateway_emits_state_interrupt_before_done():
stream = FakeLangGraphThreadStream(
"abc12345",
events=[],
interrupts=[{"interrupt_id": "interrupt-1", "value": None}],
interrupted=True,
)
threads = FakeLangGraphThreadsClient(
threads=[],
states={
"abc12345": {
"values": {},
"interrupts": [
{
"id": "interrupt-1",
"value": {
"action_requests": [
{
"name": "execute",
"args": {"command": "echo hello"},
"id": "tool-1",
}
],
"review_configs": [
{
"action_name": "execute",
"allowed_decisions": ["approve", "reject"],
}
],
},
}
],
}
},
streams={"abc12345": stream},
)
gateway = LangGraphServerGateway(
LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
)
async def _collect():
return [
event
async for event in gateway.stream_events(
RunRequest(message="hi", thread_id="abc12345")
)
]
events = await _collect()
assert events == [
{
"type": "interrupt",
"interrupt_id": "interrupt-1",
"action_requests": [
{
"name": "execute",
"args": {"command": "echo hello"},
"id": "tool-1",
}
],
"review_configs": [
{
"action_name": "execute",
"allowed_decisions": ["approve", "reject"],
}
],
},
{"type": "done", "content": "", "response": ""},
]
async def test_langgraph_server_gateway_streams_subagent_protocol_events():
stream = FakeLangGraphThreadStream(
"abc12345",
events=[
{
"method": "lifecycle",
"params": {
"namespace": ["data-analysis-agent:tool-1"],
"data": {"event": "started"},
},
},
{
"method": "messages",
"params": {
"namespace": ["data-analysis-agent:tool-1"],
"data": {
"event": "content-block-delta",
"delta": {"type": "text-delta", "text": "sub text"},
},
},
},
{
"method": "lifecycle",
"params": {
"namespace": ["data-analysis-agent:tool-1"],
"data": {"event": "completed"},
},
},
],
)
threads = FakeLangGraphThreadsClient(
threads=[{"thread_id": "abc12345", "metadata": {"graph_id": "EvoScientist"}}],
states={"abc12345": {"values": {}}},
streams={"abc12345": stream},
)
gateway = LangGraphServerGateway(
LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
)
async def _collect():
return [
event
async for event in gateway.stream_events(
RunRequest(message="hi", thread_id="abc12345")
)
]
events = await _collect()
assert events == [
{
"type": "subagent_start",
"name": "data-analysis-agent",
"description": "",
"instance_id": "data-analysis-agent:tool-1",
"tool_call_id": "tool-1",
},
{
"type": "subagent_text",
"subagent": "data-analysis-agent",
"content": "sub text",
"instance_id": "data-analysis-agent:tool-1",
},
{
"type": "subagent_end",
"name": "data-analysis-agent",
"instance_id": "data-analysis-agent:tool-1",
},
{"type": "done", "content": "", "response": ""},
]
async def test_langgraph_server_gateway_resumes_interrupt_with_thread_stream():
from langgraph.types import Command
stream = FakeLangGraphThreadStream(
"abc12345",
events=[],
interrupts=[{"interrupt_id": "interrupt-1"}],
)
threads = FakeLangGraphThreadsClient(
threads=[{"thread_id": "abc12345", "metadata": {"graph_id": "EvoScientist"}}],
states={"abc12345": {"values": {}}},
streams={"abc12345": stream},
)
gateway = LangGraphServerGateway(
LangGraphServerThreadStore(
client=FakeLangGraphClient(threads),
)
)
async def _collect():
return [
event
async for event in gateway.stream_events(
RunRequest(
message=Command(resume={"decisions": [{"allowed": True}]}),
thread_id="abc12345",
)
)
]
events = await _collect()
assert stream.run.starts == []
assert stream.run.responses == [
{
"response": {"decisions": [{"allowed": True}]},
"interrupt_id": "interrupt-1",
}
]
assert events == [{"type": "done", "content": "", "response": ""}]