mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
836e7c786f
The matrix covered the declared floor (0.0.42) and the newest release (0.0.44) but skipped 0.0.43 — the only still-supported version that reproduces the failure this PR fixes. 0.0.43's `LangGraphAgent.clone()` passes its three behavior flags to `type(self)(...)` unconditionally, so a subclass with a closed signature raises TypeError on the default construction path — a 500 on every request, since the FastAPI endpoint clones per request. 0.0.44's `clone()` is signature-aware and omits default-valued flags a subclass cannot accept, which means the clone tests pass on 0.0.44 even with the `**kwargs` passthrough removed. The `emit_raw_events=False` test still guards option reachability there, but nothing in the matrix reproduced the default-construction 500 itself. Verified locally against 0.0.43: all four clone tests pass with the passthrough and all four fail without it, so the leg is a real guard. One representative Python (3.12) via `matrix.include` rather than a third full column — the flag forwarding it exercises is not version-specific, so this adds one leg, not five. Installed in the leg rather than declared, so the runtime floor stays at 0.0.42 and the effective LangGraph floor stays at >=0.3.25 for consumers. Also corrects the `test_agui_agent_clone.py` module docstring, which claimed CI exercised 0.0.42 and 0.0.43 while the workflow installed 0.0.42 and 0.0.44. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
111 lines
4.4 KiB
Python
111 lines
4.4 KiB
Python
"""Tests for LangGraphAGUIAgent under the per-request clone().
|
|
|
|
``add_langgraph_fastapi_endpoint`` clones the agent on every request, so
|
|
``clone()`` is on the hot path for every self-hosted LangGraph deployment — a
|
|
failure there is a 500 on every request, not a startup error.
|
|
|
|
``LangGraphAgent.clone()`` reconstructs via ``type(self)(...)`` and forwards
|
|
whatever behavior flags the base defines, so each of them is a keyword argument
|
|
this subclass must be able to receive — which is why ``__init__`` forwards
|
|
**kwargs upstream rather than restating the base signature.
|
|
|
|
These tests deliberately assert behavior rather than upstream specifics: no
|
|
enumerated flag names, no error-message text, no traceback locations. The
|
|
supported range is ``ag-ui-langgraph>=0.0.42``, and CI exercises the declared
|
|
floor 0.0.42, the 0.0.43 whose ``clone()`` forwards its flags unconditionally,
|
|
and the newest 0.0.44, so anything version-dependent is guarded rather than
|
|
assumed.
|
|
"""
|
|
|
|
import inspect
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from ag_ui.core import EventType, TextMessageContentEvent
|
|
from ag_ui_langgraph import LangGraphAgent as AGUIBase
|
|
|
|
from copilotkit.langgraph_agui_agent import LangGraphAGUIAgent
|
|
|
|
|
|
BASE_INIT_PARAMS = inspect.signature(AGUIBase.__init__).parameters
|
|
|
|
|
|
def _make_graph():
|
|
graph = MagicMock()
|
|
graph.nodes = {}
|
|
return graph
|
|
|
|
|
|
def _raw_event_message():
|
|
"""A content event carrying the piggy-backed raw_event ``emit_raw_events`` gates."""
|
|
return TextMessageContentEvent(
|
|
type=EventType.TEXT_MESSAGE_CONTENT,
|
|
message_id="msg-1",
|
|
delta="hi",
|
|
raw_event={"event": "on_chat_model_stream"},
|
|
)
|
|
|
|
|
|
def test_clone_succeeds():
|
|
"""clone() must not raise, whatever flags the installed base forwards.
|
|
|
|
A closed signature rejects them, and since the endpoint clones per request
|
|
that is a 500 on every request.
|
|
"""
|
|
agent = LangGraphAGUIAgent(name="test", graph=_make_graph())
|
|
cloned = agent.clone()
|
|
assert isinstance(cloned, LangGraphAGUIAgent)
|
|
assert cloned is not agent
|
|
|
|
|
|
def test_clone_preserves_copilotkit_state_namespace():
|
|
"""The copilotkit schema key must survive the per-request clone."""
|
|
agent = LangGraphAGUIAgent(name="test", graph=_make_graph())
|
|
cloned = agent.clone()
|
|
assert "copilotkit" in cloned.constant_schema_keys
|
|
assert cloned.constant_schema_keys.count("copilotkit") == 1
|
|
|
|
|
|
def test_clone_isolates_per_request_state_from_the_source():
|
|
"""The clone starts with a fresh run scope and the source keeps its own.
|
|
|
|
The endpoint holds one long-lived template agent and clones it per request,
|
|
so both halves are observable. The clone must not inherit the template's
|
|
run-local state — a ``copy.copy``-style clone would hand request N+1 the
|
|
state of request N — and cloning must not disturb the template either, or
|
|
the template stops being a clean starting point for every later request.
|
|
"""
|
|
agent = LangGraphAGUIAgent(name="test", graph=_make_graph())
|
|
agent.active_run = {"id": "run-1"}
|
|
agent._copilotkit_runtime_payload = {"actions": [], "context": []}
|
|
|
|
cloned = agent.clone()
|
|
|
|
assert cloned.active_run is None
|
|
assert cloned._copilotkit_runtime_payload is None
|
|
assert agent.active_run == {"id": "run-1"}
|
|
assert agent._copilotkit_runtime_payload == {"actions": [], "context": []}
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
"emit_raw_events" not in BASE_INIT_PARAMS,
|
|
reason="installed ag-ui-langgraph predates emit_raw_events",
|
|
)
|
|
def test_clone_carries_a_non_default_upstream_option():
|
|
"""A non-default base option must be settable here, and survive the clone.
|
|
|
|
Without the passthrough, ``emit_raw_events=False`` — the OSS-607 payload
|
|
opt-out — cannot be set by any CopilotKit user at all. Asserted through
|
|
``_dispatch_event`` rather than through the attribute: an attribute
|
|
assertion still passes if the option stops being applied to outgoing
|
|
events. Two-sided on purpose, since the opt-out direction alone also passes
|
|
in a world where raw events are dropped for everybody.
|
|
"""
|
|
default_clone = LangGraphAGUIAgent(name="test", graph=_make_graph()).clone()
|
|
opted_out_clone = LangGraphAGUIAgent(
|
|
name="test", graph=_make_graph(), emit_raw_events=False
|
|
).clone()
|
|
|
|
assert default_clone._dispatch_event(_raw_event_message()).raw_event is not None
|
|
assert opted_out_clone._dispatch_event(_raw_event_message()).raw_event is None
|