Files

111 lines
4.4 KiB
Python
Raw Permalink Normal View History

fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone() add_langgraph_fastapi_endpoint clones the agent on every request, and LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only signature, so ag-ui-langgraph 0.0.43 - which began forwarding enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events - made every request 500 on the documented LangGraph + FastAPI quickstart: TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__ accepts (name, graph, description, config) as keyword arguments: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'enable_legacy_on_interrupt_event' Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a fresh install already resolves to 0.0.43 and fails on the first message. __init__ now forwards **kwargs upstream instead of restating the base signature. That fixes the 500 without waiting on an upstream release, and makes base flags reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not be set by any CopilotKit user through this subclass. Also constructs the intercepted-tool-call test double for real rather than via object.__new__. That helper built an instance with no behavior flags set, so it raised AttributeError out of the base dispatch path the moment ag-ui-langgraph started reading one - 4 failures the pinned 0.0.42 lockfile currently hides. Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235 passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed / 13 skipped (the two flag tests skip by design). End to end, two sequential POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no RUN_ERROR, where the published packages raise at endpoint.py:23. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
"""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.
test(sdk-python): make the clone() kwargs guards falsifiable Three assertions in test_agui_agent_clone.py passed whether or not the code they name was correct. Each was found independently by a reviewer. 1. test_init_rejects_unknown_kwarg_end_to_end used a bare pytest.raises(TypeError). The pre-fix closed 4-parameter signature raises TypeError for a bogus kwarg too, from the subclass frame, so the assertion could not distinguish "forwarded to the base, which rejected it" from "rejected locally before forwarding" — reverting the **kwargs passthrough left it green. Renamed to test_init_rejects_unknown_kwarg_in_the_base_not_locally and given two origin checks: match= on the BASE class's __init__ qualname (a closed signature reports LangGraphAGUIAgent.__init__, not LangGraphAgent.__init__) plus an assertion that the innermost traceback frame is the subclass's own super().__init__ call site rather than the test module's construction call. The match= also fixes the second half of the finding: an unrelated TypeError on this path (the base's clone() re-raises construction failures as TypeError) can no longer satisfy it. 2. test_clone_carries_upstream_flags asserted only the opted-out direction, so it also passed in a world where emit_raw_events was forced False for everyone — the inverse of the opt-out's intent. It now builds a default clone as an in-test control and asserts both directions, rather than borrowing the control from its sibling that the paired skipif silences at the same time. 3. test_clone_resets_per_request_state mutated the source agent in setup and then asserted the clone's attributes are None — true for any instance built through __init__, so the setup was inert. Renamed to test_clone_isolates_per_request_state_from_the_source and given the assertions that make the setup load-bearing: the source keeps its own run-local state across the clone. The two None assertions stay because they are falsifiable (a copy.copy-style clone fails them), not decorative; mutation M9 below proves it. Also in this file: - The 0.0.43 flag names lived in prose in test_clone_succeeds's docstring, referenced by no test body, reading as exhaustive. They are now CLONE_FORWARDED_FLAGS_0_0_43 and are asserted through the base __init__ spy, so the list is live data; the comment says outright that it illustrates the problem rather than staying exhaustive. - test_init_forwards_unknown_kwargs_to_base (renamed test_init_forwards_base_kwargs) now also asserts description and config reach the base. The spy made it a two-line addition, and it closes the gap that nothing pinned two of the four parameters __init__ names. - The TextMessageContentEvent stub was built twice, once inline and once in a nested closure; it is now one module-level _raw_event_message() helper. NOTE: the graph stub is NOT duplicated in this file — _make_graph is already the single local helper and all six call sites use it. The duplication a reviewer flagged is across this file and test_intercepted_tool_call_events.py, which is logged as deferred and untouched here. Both skipif guards are kept deliberately. The declared floor is ag-ui-langgraph >=0.0.42, where emit_raw_events does not exist, so skipping is honest at the floor; test_init_forwards_base_kwargs is the guard that cannot skip on any version. Tests only. No change to copilotkit/langgraph_agui_agent.py, pyproject.toml or any lockfile. Test count is unchanged at 7 in this file. Mutation Evidence Every added or changed assertion was mutation-tested: the guarded behavior was broken, RED confirmed, source restored, GREEN confirmed. Source mutations were applied to copilotkit/langgraph_agui_agent.py; M9/M10 to the installed ag_ui_langgraph/agent.py. Both files restored by checksum afterwards. M1 **kwargs passthrough reverted to the closed 4-parameter signature — the exact scenario the old assertion could not detect. @0.0.42 (the declared floor, what CI installs): NEW file -> 2 failed, 3 passed, 2 skipped (test_init_rejects_unknown_kwarg_in_the_base_not_locally FAILED: "Expected regex: LangGraphAgent\.__init__\(\) got an unexpected keyword argument 'not_a_real_flag_the_base_defines' Actual message: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'not_a_real_flag_the_base_defines'") PRE-FIX body of the same test -> PASSED. That is the defect. @0.0.43: NEW file -> 7 failed; PRE-FIX bare-raises body -> PASSED. M2 description=None in the super() call -> test_init_forwards_base_kwargs FAILED (1 failed, 6 passed). M3 config=None in the super() call -> test_init_forwards_base_kwargs FAILED. M4 name mangled on the way to the base -> test_init_forwards_base_kwargs FAILED. M5 graph not relayed -> 6 failed, 1 passed. M6 emit_raw_events forced False for everyone -> test_init_forwards_upstream_ flags AND test_clone_carries_upstream_flags FAILED. The PRE-FIX one-sided clone body PASSED under the same mutation. That is defect 2. M7 user's emit_raw_events opt-out overwritten with True -> both flag tests FAILED (the other direction). M8 emit_interrupt_outcome filtered out of the passthrough -> test_init_forwards_base_kwargs FAILED with KeyError. Proves the flag list is live data, not a comment. M9 base clone() replaced with copy.copy(self) -> test_clone_isolates_per_request_state_from_the_source FAILED. Proves the two None assertions are falsifiable and worth keeping. M10 base clone() wipes the SOURCE's run scope -> test_clone_isolates_per_request_state_from_the_source FAILED, while the PRE-FIX tautology body PASSED. That is defect 3. Verification Full sdk-python suite, Python 3.12 uv venv, ruff format --check clean: ag-ui-langgraph 0.0.43 -> 237 passed, 11 skipped ag-ui-langgraph 0.0.42 -> 235 passed, 13 skipped Identical to the pre-change baseline on both versions, so no test count decreased and the 0.0.42 delta is still only the two honest emit_raw_events skips. `ruff check` still reports the same pre-existing I001 it reported on the committed file (no ruff config or ruff step exists for sdk-python), so it is left untouched. git status clean apart from this one test file; neither pyproject.toml nor any lockfile appears in the diff. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 18:01:52 +02:00
``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
ci(sdk-python): add an ag-ui-langgraph 0.0.43 regression leg 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>
2026-09-02 16:22:30 +02:00
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.
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone() add_langgraph_fastapi_endpoint clones the agent on every request, and LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only signature, so ag-ui-langgraph 0.0.43 - which began forwarding enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events - made every request 500 on the documented LangGraph + FastAPI quickstart: TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__ accepts (name, graph, description, config) as keyword arguments: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'enable_legacy_on_interrupt_event' Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a fresh install already resolves to 0.0.43 and fails on the first message. __init__ now forwards **kwargs upstream instead of restating the base signature. That fixes the 500 without waiting on an upstream release, and makes base flags reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not be set by any CopilotKit user through this subclass. Also constructs the intercepted-tool-call test double for real rather than via object.__new__. That helper built an instance with no behavior flags set, so it raised AttributeError out of the base dispatch path the moment ag-ui-langgraph started reading one - 4 failures the pinned 0.0.42 lockfile currently hides. Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235 passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed / 13 skipped (the two flag tests skip by design). End to end, two sequential POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no RUN_ERROR, where the published packages raise at endpoint.py:23. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
"""
import inspect
from unittest.mock import MagicMock
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone() add_langgraph_fastapi_endpoint clones the agent on every request, and LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only signature, so ag-ui-langgraph 0.0.43 - which began forwarding enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events - made every request 500 on the documented LangGraph + FastAPI quickstart: TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__ accepts (name, graph, description, config) as keyword arguments: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'enable_legacy_on_interrupt_event' Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a fresh install already resolves to 0.0.43 and fails on the first message. __init__ now forwards **kwargs upstream instead of restating the base signature. That fixes the 500 without waiting on an upstream release, and makes base flags reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not be set by any CopilotKit user through this subclass. Also constructs the intercepted-tool-call test double for real rather than via object.__new__. That helper built an instance with no behavior flags set, so it raised AttributeError out of the base dispatch path the moment ag-ui-langgraph started reading one - 4 failures the pinned 0.0.42 lockfile currently hides. Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235 passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed / 13 skipped (the two flag tests skip by design). End to end, two sequential POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no RUN_ERROR, where the published packages raise at endpoint.py:23. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
import pytest
test(sdk-python): guard the clone() kwargs passthrough on any base version The previous commit's regression was not guarded by its own test suite on the dependency version CI installs. sdk-python/pyproject.toml declared ag-ui-langgraph >= 0.0.42 and poetry.lock pinned 0.0.42, but the flags clone() forwards (enable_legacy_on_interrupt_event, emit_interrupt_outcome, emit_raw_events) only exist in 0.0.43+. At 0.0.42 the two flag tests skipif- skipped and the remaining three passed against the OLD closed signature too: reverting the **kwargs fix still produced "11 passed, 2 skipped", so CI would have gone green on a branch with the fix removed. Four changes close that: 1. test_init_forwards_unknown_kwargs_to_base patches the base __init__ with a recorder and asserts an arbitrary unknown kwarg reaches it. It observes the passthrough itself rather than whichever flags the installed base happens to define, so it cannot skip and it fails on ANY base version if the subclass goes back to a closed signature. This is the guard that would have caught the regression at the pinned 0.0.42. 2. test_init_rejects_unknown_kwarg_end_to_end asserts a bogus kwarg raises TypeError with no spy in place — **kwargs must forward to super() and let it reject typos, never swallow them. 3. The two emit_raw_events tests now assert the EFFECT (a piggy-backed raw_event is stripped by _dispatch_event) instead of the attribute value. An attribute assertion cannot fail if the flag stops being honored. 4. The ag-ui-langgraph floor moves to >=0.0.43 (regenerated poetry.lock) so those behavioral tests stop skipping in CI at all. Also drops a dead `graph.get_state = MagicMock()` from the local _make_graph; nothing in the exercised paths reads it (MagicMock auto-creates it anyway). Verification Red-green, fix reverted to the closed 4-kwarg signature: ag-ui-langgraph 0.0.42 -> 1 failed, 4 passed, 2 skipped (test_init_forwards_unknown_kwargs_to_base FAILED — previously this state was fully green, which is the hole being closed) ag-ui-langgraph 0.0.43 -> 6 failed, 1 passed Fix restored, full sdk-python suite: 0.0.43 (Python 3.12 uv venv) -> 237 passed, 11 skipped 0.0.43 (poetry install --with dev, 3.13) -> 237 passed, 11 skipped 0.0.42 (Python 3.12 uv venv) -> 235 passed, 13 skipped The only delta between 0.0.42 and 0.0.43 is the 2 emit_raw_events skips, so the floor bump is the sole thing forcing 0.0.43. `poetry lock` is idempotent on the committed lock (CI runs it before install) and `poetry check --lock` exits 0. `ruff format` leaves the test file unchanged. uv.lock is byte-identical (it locks only the dev group and contains no ag-ui-langgraph entry; CI uses poetry, not uv). Call-Site Enumeration _make_graph (tests/test_agui_agent_clone.py) — CHANGED (get_state line removed). 8 call sites, all in this file (the other test modules each have their own local mock-graph builder). Assumption still holds: the removed line only pre-set an attribute MagicMock creates on demand; both full suites are green. test_init_forwards_unknown_kwargs_to_base, test_init_rejects_unknown_kwarg_ end_to_end — ADDED. No references outside pytest collection. test_init_forwards_upstream_flags, test_clone_carries_upstream_flags — CHANGED bodies only. No references outside pytest collection. BASE_INIT_PARAMS / `import inspect` — UNCHANGED and still live; both skipif markers still read them. Kept deliberately: with the floor at >=0.0.43 they can no longer fire on a supported install, and the version-independent guard above cannot skip regardless, so they only protect a stale local env. `patch`, `EventType`, `TextMessageContentEvent` — ADDED imports, used only in this file. `MagicMock` still used by _make_graph. ag-ui-langgraph constraint (sdk-python/pyproject.toml) — CHANGED. Consumers in-repo: examples/integrations/langgraph-fastapi/agent/pyproject.toml and the langgraph-fastapi / langgraph-python Dockerfiles pin PUBLISHED copilotkit (0.1.93/0.1.94) alongside their own ag-ui-langgraph pins (0.0.41/0.0.37), so they do not resolve this constraint and are unaffected today; when they next bump copilotkit past the release carrying this change their own pins will need to move to >=0.0.43. examples/v1/travel and examples/showcases/* depend on ag-ui-langgraph directly, not through sdk-python. No in-repo target installs copilotkit from this path. sdk-python/poetry.lock — REGENERATED. Read by .github/workflows/ test_unit-python-sdk.yml (`poetry lock && poetry install --with dev`); resolves ag-ui-langgraph 0.0.43. metadata.python-versions stays ">=3.10,<3.15" and uv.lock's requires-python is untouched, so Python 3.10 and 3.11 support is unchanged. Beyond the ag-ui-langgraph bump and its transitive langgraph >=0.6.0 floor (already satisfied by locked 1.1.10), the regeneration also (a) rewrote the generator header 2.1.3 -> 2.3.2, (b) added the previously unlocked [dependency-groups] dev packages (pytest, pytest-asyncio, pluggy, iniconfig, backports-asyncio-runner) and the corresponding main+dev group markers, and (c) normalized a few specifier spellings (2023.03.6 -> 2023.3.6, 14.05.14 -> 14.5.14, 2.0.0b -> 2.0.0b0). All three are artifacts of re-resolving with current Poetry rather than of the dependency change; none alter a resolved main dependency version. sdk-python/pyproject.toml is also read by .github/workflows/ publish-release.yml (build-python fires on any merged PR touching it). Assumption still holds: detect-py-version-changes.sh gates on the version field, which is unchanged at 0.1.95, so the lane reports "Nothing to publish". Publishing this fix still needs a separate version bump. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 17:23:25 +02:00
from ag_ui.core import EventType, TextMessageContentEvent
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone() add_langgraph_fastapi_endpoint clones the agent on every request, and LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only signature, so ag-ui-langgraph 0.0.43 - which began forwarding enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events - made every request 500 on the documented LangGraph + FastAPI quickstart: TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__ accepts (name, graph, description, config) as keyword arguments: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'enable_legacy_on_interrupt_event' Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a fresh install already resolves to 0.0.43 and fails on the first message. __init__ now forwards **kwargs upstream instead of restating the base signature. That fixes the 500 without waiting on an upstream release, and makes base flags reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not be set by any CopilotKit user through this subclass. Also constructs the intercepted-tool-call test double for real rather than via object.__new__. That helper built an instance with no behavior flags set, so it raised AttributeError out of the base dispatch path the moment ag-ui-langgraph started reading one - 4 failures the pinned 0.0.42 lockfile currently hides. Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235 passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed / 13 skipped (the two flag tests skip by design). End to end, two sequential POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no RUN_ERROR, where the published packages raise at endpoint.py:23. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
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
test(sdk-python): make the clone() kwargs guards falsifiable Three assertions in test_agui_agent_clone.py passed whether or not the code they name was correct. Each was found independently by a reviewer. 1. test_init_rejects_unknown_kwarg_end_to_end used a bare pytest.raises(TypeError). The pre-fix closed 4-parameter signature raises TypeError for a bogus kwarg too, from the subclass frame, so the assertion could not distinguish "forwarded to the base, which rejected it" from "rejected locally before forwarding" — reverting the **kwargs passthrough left it green. Renamed to test_init_rejects_unknown_kwarg_in_the_base_not_locally and given two origin checks: match= on the BASE class's __init__ qualname (a closed signature reports LangGraphAGUIAgent.__init__, not LangGraphAgent.__init__) plus an assertion that the innermost traceback frame is the subclass's own super().__init__ call site rather than the test module's construction call. The match= also fixes the second half of the finding: an unrelated TypeError on this path (the base's clone() re-raises construction failures as TypeError) can no longer satisfy it. 2. test_clone_carries_upstream_flags asserted only the opted-out direction, so it also passed in a world where emit_raw_events was forced False for everyone — the inverse of the opt-out's intent. It now builds a default clone as an in-test control and asserts both directions, rather than borrowing the control from its sibling that the paired skipif silences at the same time. 3. test_clone_resets_per_request_state mutated the source agent in setup and then asserted the clone's attributes are None — true for any instance built through __init__, so the setup was inert. Renamed to test_clone_isolates_per_request_state_from_the_source and given the assertions that make the setup load-bearing: the source keeps its own run-local state across the clone. The two None assertions stay because they are falsifiable (a copy.copy-style clone fails them), not decorative; mutation M9 below proves it. Also in this file: - The 0.0.43 flag names lived in prose in test_clone_succeeds's docstring, referenced by no test body, reading as exhaustive. They are now CLONE_FORWARDED_FLAGS_0_0_43 and are asserted through the base __init__ spy, so the list is live data; the comment says outright that it illustrates the problem rather than staying exhaustive. - test_init_forwards_unknown_kwargs_to_base (renamed test_init_forwards_base_kwargs) now also asserts description and config reach the base. The spy made it a two-line addition, and it closes the gap that nothing pinned two of the four parameters __init__ names. - The TextMessageContentEvent stub was built twice, once inline and once in a nested closure; it is now one module-level _raw_event_message() helper. NOTE: the graph stub is NOT duplicated in this file — _make_graph is already the single local helper and all six call sites use it. The duplication a reviewer flagged is across this file and test_intercepted_tool_call_events.py, which is logged as deferred and untouched here. Both skipif guards are kept deliberately. The declared floor is ag-ui-langgraph >=0.0.42, where emit_raw_events does not exist, so skipping is honest at the floor; test_init_forwards_base_kwargs is the guard that cannot skip on any version. Tests only. No change to copilotkit/langgraph_agui_agent.py, pyproject.toml or any lockfile. Test count is unchanged at 7 in this file. Mutation Evidence Every added or changed assertion was mutation-tested: the guarded behavior was broken, RED confirmed, source restored, GREEN confirmed. Source mutations were applied to copilotkit/langgraph_agui_agent.py; M9/M10 to the installed ag_ui_langgraph/agent.py. Both files restored by checksum afterwards. M1 **kwargs passthrough reverted to the closed 4-parameter signature — the exact scenario the old assertion could not detect. @0.0.42 (the declared floor, what CI installs): NEW file -> 2 failed, 3 passed, 2 skipped (test_init_rejects_unknown_kwarg_in_the_base_not_locally FAILED: "Expected regex: LangGraphAgent\.__init__\(\) got an unexpected keyword argument 'not_a_real_flag_the_base_defines' Actual message: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'not_a_real_flag_the_base_defines'") PRE-FIX body of the same test -> PASSED. That is the defect. @0.0.43: NEW file -> 7 failed; PRE-FIX bare-raises body -> PASSED. M2 description=None in the super() call -> test_init_forwards_base_kwargs FAILED (1 failed, 6 passed). M3 config=None in the super() call -> test_init_forwards_base_kwargs FAILED. M4 name mangled on the way to the base -> test_init_forwards_base_kwargs FAILED. M5 graph not relayed -> 6 failed, 1 passed. M6 emit_raw_events forced False for everyone -> test_init_forwards_upstream_ flags AND test_clone_carries_upstream_flags FAILED. The PRE-FIX one-sided clone body PASSED under the same mutation. That is defect 2. M7 user's emit_raw_events opt-out overwritten with True -> both flag tests FAILED (the other direction). M8 emit_interrupt_outcome filtered out of the passthrough -> test_init_forwards_base_kwargs FAILED with KeyError. Proves the flag list is live data, not a comment. M9 base clone() replaced with copy.copy(self) -> test_clone_isolates_per_request_state_from_the_source FAILED. Proves the two None assertions are falsifiable and worth keeping. M10 base clone() wipes the SOURCE's run scope -> test_clone_isolates_per_request_state_from_the_source FAILED, while the PRE-FIX tautology body PASSED. That is defect 3. Verification Full sdk-python suite, Python 3.12 uv venv, ruff format --check clean: ag-ui-langgraph 0.0.43 -> 237 passed, 11 skipped ag-ui-langgraph 0.0.42 -> 235 passed, 13 skipped Identical to the pre-change baseline on both versions, so no test count decreased and the 0.0.42 delta is still only the two honest emit_raw_events skips. `ruff check` still reports the same pre-existing I001 it reported on the committed file (no ruff config or ruff step exists for sdk-python), so it is left untouched. git status clean apart from this one test file; neither pyproject.toml nor any lockfile appears in the diff. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 18:01:52 +02:00
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"},
)
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone() add_langgraph_fastapi_endpoint clones the agent on every request, and LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only signature, so ag-ui-langgraph 0.0.43 - which began forwarding enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events - made every request 500 on the documented LangGraph + FastAPI quickstart: TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__ accepts (name, graph, description, config) as keyword arguments: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'enable_legacy_on_interrupt_event' Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a fresh install already resolves to 0.0.43 and fails on the first message. __init__ now forwards **kwargs upstream instead of restating the base signature. That fixes the 500 without waiting on an upstream release, and makes base flags reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not be set by any CopilotKit user through this subclass. Also constructs the intercepted-tool-call test double for real rather than via object.__new__. That helper built an instance with no behavior flags set, so it raised AttributeError out of the base dispatch path the moment ag-ui-langgraph started reading one - 4 failures the pinned 0.0.42 lockfile currently hides. Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235 passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed / 13 skipped (the two flag tests skip by design). End to end, two sequential POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no RUN_ERROR, where the published packages raise at endpoint.py:23. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
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.
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone() add_langgraph_fastapi_endpoint clones the agent on every request, and LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only signature, so ag-ui-langgraph 0.0.43 - which began forwarding enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events - made every request 500 on the documented LangGraph + FastAPI quickstart: TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__ accepts (name, graph, description, config) as keyword arguments: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'enable_legacy_on_interrupt_event' Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a fresh install already resolves to 0.0.43 and fails on the first message. __init__ now forwards **kwargs upstream instead of restating the base signature. That fixes the 500 without waiting on an upstream release, and makes base flags reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not be set by any CopilotKit user through this subclass. Also constructs the intercepted-tool-call test double for real rather than via object.__new__. That helper built an instance with no behavior flags set, so it raised AttributeError out of the base dispatch path the moment ag-ui-langgraph started reading one - 4 failures the pinned 0.0.42 lockfile currently hides. Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235 passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed / 13 skipped (the two flag tests skip by design). End to end, two sequential POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no RUN_ERROR, where the published packages raise at endpoint.py:23. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
"""
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
test(sdk-python): make the clone() kwargs guards falsifiable Three assertions in test_agui_agent_clone.py passed whether or not the code they name was correct. Each was found independently by a reviewer. 1. test_init_rejects_unknown_kwarg_end_to_end used a bare pytest.raises(TypeError). The pre-fix closed 4-parameter signature raises TypeError for a bogus kwarg too, from the subclass frame, so the assertion could not distinguish "forwarded to the base, which rejected it" from "rejected locally before forwarding" — reverting the **kwargs passthrough left it green. Renamed to test_init_rejects_unknown_kwarg_in_the_base_not_locally and given two origin checks: match= on the BASE class's __init__ qualname (a closed signature reports LangGraphAGUIAgent.__init__, not LangGraphAgent.__init__) plus an assertion that the innermost traceback frame is the subclass's own super().__init__ call site rather than the test module's construction call. The match= also fixes the second half of the finding: an unrelated TypeError on this path (the base's clone() re-raises construction failures as TypeError) can no longer satisfy it. 2. test_clone_carries_upstream_flags asserted only the opted-out direction, so it also passed in a world where emit_raw_events was forced False for everyone — the inverse of the opt-out's intent. It now builds a default clone as an in-test control and asserts both directions, rather than borrowing the control from its sibling that the paired skipif silences at the same time. 3. test_clone_resets_per_request_state mutated the source agent in setup and then asserted the clone's attributes are None — true for any instance built through __init__, so the setup was inert. Renamed to test_clone_isolates_per_request_state_from_the_source and given the assertions that make the setup load-bearing: the source keeps its own run-local state across the clone. The two None assertions stay because they are falsifiable (a copy.copy-style clone fails them), not decorative; mutation M9 below proves it. Also in this file: - The 0.0.43 flag names lived in prose in test_clone_succeeds's docstring, referenced by no test body, reading as exhaustive. They are now CLONE_FORWARDED_FLAGS_0_0_43 and are asserted through the base __init__ spy, so the list is live data; the comment says outright that it illustrates the problem rather than staying exhaustive. - test_init_forwards_unknown_kwargs_to_base (renamed test_init_forwards_base_kwargs) now also asserts description and config reach the base. The spy made it a two-line addition, and it closes the gap that nothing pinned two of the four parameters __init__ names. - The TextMessageContentEvent stub was built twice, once inline and once in a nested closure; it is now one module-level _raw_event_message() helper. NOTE: the graph stub is NOT duplicated in this file — _make_graph is already the single local helper and all six call sites use it. The duplication a reviewer flagged is across this file and test_intercepted_tool_call_events.py, which is logged as deferred and untouched here. Both skipif guards are kept deliberately. The declared floor is ag-ui-langgraph >=0.0.42, where emit_raw_events does not exist, so skipping is honest at the floor; test_init_forwards_base_kwargs is the guard that cannot skip on any version. Tests only. No change to copilotkit/langgraph_agui_agent.py, pyproject.toml or any lockfile. Test count is unchanged at 7 in this file. Mutation Evidence Every added or changed assertion was mutation-tested: the guarded behavior was broken, RED confirmed, source restored, GREEN confirmed. Source mutations were applied to copilotkit/langgraph_agui_agent.py; M9/M10 to the installed ag_ui_langgraph/agent.py. Both files restored by checksum afterwards. M1 **kwargs passthrough reverted to the closed 4-parameter signature — the exact scenario the old assertion could not detect. @0.0.42 (the declared floor, what CI installs): NEW file -> 2 failed, 3 passed, 2 skipped (test_init_rejects_unknown_kwarg_in_the_base_not_locally FAILED: "Expected regex: LangGraphAgent\.__init__\(\) got an unexpected keyword argument 'not_a_real_flag_the_base_defines' Actual message: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'not_a_real_flag_the_base_defines'") PRE-FIX body of the same test -> PASSED. That is the defect. @0.0.43: NEW file -> 7 failed; PRE-FIX bare-raises body -> PASSED. M2 description=None in the super() call -> test_init_forwards_base_kwargs FAILED (1 failed, 6 passed). M3 config=None in the super() call -> test_init_forwards_base_kwargs FAILED. M4 name mangled on the way to the base -> test_init_forwards_base_kwargs FAILED. M5 graph not relayed -> 6 failed, 1 passed. M6 emit_raw_events forced False for everyone -> test_init_forwards_upstream_ flags AND test_clone_carries_upstream_flags FAILED. The PRE-FIX one-sided clone body PASSED under the same mutation. That is defect 2. M7 user's emit_raw_events opt-out overwritten with True -> both flag tests FAILED (the other direction). M8 emit_interrupt_outcome filtered out of the passthrough -> test_init_forwards_base_kwargs FAILED with KeyError. Proves the flag list is live data, not a comment. M9 base clone() replaced with copy.copy(self) -> test_clone_isolates_per_request_state_from_the_source FAILED. Proves the two None assertions are falsifiable and worth keeping. M10 base clone() wipes the SOURCE's run scope -> test_clone_isolates_per_request_state_from_the_source FAILED, while the PRE-FIX tautology body PASSED. That is defect 3. Verification Full sdk-python suite, Python 3.12 uv venv, ruff format --check clean: ag-ui-langgraph 0.0.43 -> 237 passed, 11 skipped ag-ui-langgraph 0.0.42 -> 235 passed, 13 skipped Identical to the pre-change baseline on both versions, so no test count decreased and the 0.0.42 delta is still only the two honest emit_raw_events skips. `ruff check` still reports the same pre-existing I001 it reported on the committed file (no ruff config or ruff step exists for sdk-python), so it is left untouched. git status clean apart from this one test file; neither pyproject.toml nor any lockfile appears in the diff. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 18:01:52 +02:00
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.
"""
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone() add_langgraph_fastapi_endpoint clones the agent on every request, and LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only signature, so ag-ui-langgraph 0.0.43 - which began forwarding enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events - made every request 500 on the documented LangGraph + FastAPI quickstart: TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__ accepts (name, graph, description, config) as keyword arguments: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'enable_legacy_on_interrupt_event' Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a fresh install already resolves to 0.0.43 and fails on the first message. __init__ now forwards **kwargs upstream instead of restating the base signature. That fixes the 500 without waiting on an upstream release, and makes base flags reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not be set by any CopilotKit user through this subclass. Also constructs the intercepted-tool-call test double for real rather than via object.__new__. That helper built an instance with no behavior flags set, so it raised AttributeError out of the base dispatch path the moment ag-ui-langgraph started reading one - 4 failures the pinned 0.0.42 lockfile currently hides. Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235 passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed / 13 skipped (the two flag tests skip by design). End to end, two sequential POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no RUN_ERROR, where the published packages raise at endpoint.py:23. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
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
test(sdk-python): make the clone() kwargs guards falsifiable Three assertions in test_agui_agent_clone.py passed whether or not the code they name was correct. Each was found independently by a reviewer. 1. test_init_rejects_unknown_kwarg_end_to_end used a bare pytest.raises(TypeError). The pre-fix closed 4-parameter signature raises TypeError for a bogus kwarg too, from the subclass frame, so the assertion could not distinguish "forwarded to the base, which rejected it" from "rejected locally before forwarding" — reverting the **kwargs passthrough left it green. Renamed to test_init_rejects_unknown_kwarg_in_the_base_not_locally and given two origin checks: match= on the BASE class's __init__ qualname (a closed signature reports LangGraphAGUIAgent.__init__, not LangGraphAgent.__init__) plus an assertion that the innermost traceback frame is the subclass's own super().__init__ call site rather than the test module's construction call. The match= also fixes the second half of the finding: an unrelated TypeError on this path (the base's clone() re-raises construction failures as TypeError) can no longer satisfy it. 2. test_clone_carries_upstream_flags asserted only the opted-out direction, so it also passed in a world where emit_raw_events was forced False for everyone — the inverse of the opt-out's intent. It now builds a default clone as an in-test control and asserts both directions, rather than borrowing the control from its sibling that the paired skipif silences at the same time. 3. test_clone_resets_per_request_state mutated the source agent in setup and then asserted the clone's attributes are None — true for any instance built through __init__, so the setup was inert. Renamed to test_clone_isolates_per_request_state_from_the_source and given the assertions that make the setup load-bearing: the source keeps its own run-local state across the clone. The two None assertions stay because they are falsifiable (a copy.copy-style clone fails them), not decorative; mutation M9 below proves it. Also in this file: - The 0.0.43 flag names lived in prose in test_clone_succeeds's docstring, referenced by no test body, reading as exhaustive. They are now CLONE_FORWARDED_FLAGS_0_0_43 and are asserted through the base __init__ spy, so the list is live data; the comment says outright that it illustrates the problem rather than staying exhaustive. - test_init_forwards_unknown_kwargs_to_base (renamed test_init_forwards_base_kwargs) now also asserts description and config reach the base. The spy made it a two-line addition, and it closes the gap that nothing pinned two of the four parameters __init__ names. - The TextMessageContentEvent stub was built twice, once inline and once in a nested closure; it is now one module-level _raw_event_message() helper. NOTE: the graph stub is NOT duplicated in this file — _make_graph is already the single local helper and all six call sites use it. The duplication a reviewer flagged is across this file and test_intercepted_tool_call_events.py, which is logged as deferred and untouched here. Both skipif guards are kept deliberately. The declared floor is ag-ui-langgraph >=0.0.42, where emit_raw_events does not exist, so skipping is honest at the floor; test_init_forwards_base_kwargs is the guard that cannot skip on any version. Tests only. No change to copilotkit/langgraph_agui_agent.py, pyproject.toml or any lockfile. Test count is unchanged at 7 in this file. Mutation Evidence Every added or changed assertion was mutation-tested: the guarded behavior was broken, RED confirmed, source restored, GREEN confirmed. Source mutations were applied to copilotkit/langgraph_agui_agent.py; M9/M10 to the installed ag_ui_langgraph/agent.py. Both files restored by checksum afterwards. M1 **kwargs passthrough reverted to the closed 4-parameter signature — the exact scenario the old assertion could not detect. @0.0.42 (the declared floor, what CI installs): NEW file -> 2 failed, 3 passed, 2 skipped (test_init_rejects_unknown_kwarg_in_the_base_not_locally FAILED: "Expected regex: LangGraphAgent\.__init__\(\) got an unexpected keyword argument 'not_a_real_flag_the_base_defines' Actual message: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'not_a_real_flag_the_base_defines'") PRE-FIX body of the same test -> PASSED. That is the defect. @0.0.43: NEW file -> 7 failed; PRE-FIX bare-raises body -> PASSED. M2 description=None in the super() call -> test_init_forwards_base_kwargs FAILED (1 failed, 6 passed). M3 config=None in the super() call -> test_init_forwards_base_kwargs FAILED. M4 name mangled on the way to the base -> test_init_forwards_base_kwargs FAILED. M5 graph not relayed -> 6 failed, 1 passed. M6 emit_raw_events forced False for everyone -> test_init_forwards_upstream_ flags AND test_clone_carries_upstream_flags FAILED. The PRE-FIX one-sided clone body PASSED under the same mutation. That is defect 2. M7 user's emit_raw_events opt-out overwritten with True -> both flag tests FAILED (the other direction). M8 emit_interrupt_outcome filtered out of the passthrough -> test_init_forwards_base_kwargs FAILED with KeyError. Proves the flag list is live data, not a comment. M9 base clone() replaced with copy.copy(self) -> test_clone_isolates_per_request_state_from_the_source FAILED. Proves the two None assertions are falsifiable and worth keeping. M10 base clone() wipes the SOURCE's run scope -> test_clone_isolates_per_request_state_from_the_source FAILED, while the PRE-FIX tautology body PASSED. That is defect 3. Verification Full sdk-python suite, Python 3.12 uv venv, ruff format --check clean: ag-ui-langgraph 0.0.43 -> 237 passed, 11 skipped ag-ui-langgraph 0.0.42 -> 235 passed, 13 skipped Identical to the pre-change baseline on both versions, so no test count decreased and the 0.0.42 delta is still only the two honest emit_raw_events skips. `ruff check` still reports the same pre-existing I001 it reported on the committed file (no ruff config or ruff step exists for sdk-python), so it is left untouched. git status clean apart from this one test file; neither pyproject.toml nor any lockfile appears in the diff. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 18:01:52 +02:00
assert agent.active_run == {"id": "run-1"}
assert agent._copilotkit_runtime_payload == {"actions": [], "context": []}
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone() add_langgraph_fastapi_endpoint clones the agent on every request, and LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only signature, so ag-ui-langgraph 0.0.43 - which began forwarding enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events - made every request 500 on the documented LangGraph + FastAPI quickstart: TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__ accepts (name, graph, description, config) as keyword arguments: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'enable_legacy_on_interrupt_event' Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a fresh install already resolves to 0.0.43 and fails on the first message. __init__ now forwards **kwargs upstream instead of restating the base signature. That fixes the 500 without waiting on an upstream release, and makes base flags reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not be set by any CopilotKit user through this subclass. Also constructs the intercepted-tool-call test double for real rather than via object.__new__. That helper built an instance with no behavior flags set, so it raised AttributeError out of the base dispatch path the moment ag-ui-langgraph started reading one - 4 failures the pinned 0.0.42 lockfile currently hides. Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235 passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed / 13 skipped (the two flag tests skip by design). End to end, two sequential POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no RUN_ERROR, where the published packages raise at endpoint.py:23. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
@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.
fix(sdk-python): forward base-agent kwargs so LangGraphAGUIAgent survives clone() add_langgraph_fastapi_endpoint clones the agent on every request, and LangGraphAgent.clone() rebuilds it through type(self)(...) forwarding the base class's own behavior flags. LangGraphAGUIAgent restated a closed keyword-only signature, so ag-ui-langgraph 0.0.43 - which began forwarding enable_legacy_on_interrupt_event, emit_interrupt_outcome and emit_raw_events - made every request 500 on the documented LangGraph + FastAPI quickstart: TypeError: LangGraphAGUIAgent must override clone() or ensure its __init__ accepts (name, graph, description, config) as keyword arguments: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'enable_legacy_on_interrupt_event' Our dependency is ag-ui-langgraph[fastapi]>=0.0.42 with no upper bound, so a fresh install already resolves to 0.0.43 and fails on the first message. __init__ now forwards **kwargs upstream instead of restating the base signature. That fixes the 500 without waiting on an upstream release, and makes base flags reachable at all - emit_raw_events=False, the OSS-607 payload opt-out, could not be set by any CopilotKit user through this subclass. Also constructs the intercepted-tool-call test double for real rather than via object.__new__. That helper built an instance with no behavior flags set, so it raised AttributeError out of the base dispatch path the moment ag-ui-langgraph started reading one - 4 failures the pinned 0.0.42 lockfile currently hides. Verified against published ag-ui-langgraph, both versions: 0.0.43 gives 235 passed / 11 skipped (was 4 failed, 226 passed), and 0.0.42 gives 233 passed / 13 skipped (the two flag tests skip by design). End to end, two sequential POSTs through add_langgraph_fastapi_endpoint on 0.0.43 return HTTP 200 with no RUN_ERROR, where the published packages raise at endpoint.py:23. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 14:42:58 +02:00
Without the passthrough, ``emit_raw_events=False`` — the OSS-607 payload
test(sdk-python): guard the clone() kwargs passthrough on any base version The previous commit's regression was not guarded by its own test suite on the dependency version CI installs. sdk-python/pyproject.toml declared ag-ui-langgraph >= 0.0.42 and poetry.lock pinned 0.0.42, but the flags clone() forwards (enable_legacy_on_interrupt_event, emit_interrupt_outcome, emit_raw_events) only exist in 0.0.43+. At 0.0.42 the two flag tests skipif- skipped and the remaining three passed against the OLD closed signature too: reverting the **kwargs fix still produced "11 passed, 2 skipped", so CI would have gone green on a branch with the fix removed. Four changes close that: 1. test_init_forwards_unknown_kwargs_to_base patches the base __init__ with a recorder and asserts an arbitrary unknown kwarg reaches it. It observes the passthrough itself rather than whichever flags the installed base happens to define, so it cannot skip and it fails on ANY base version if the subclass goes back to a closed signature. This is the guard that would have caught the regression at the pinned 0.0.42. 2. test_init_rejects_unknown_kwarg_end_to_end asserts a bogus kwarg raises TypeError with no spy in place — **kwargs must forward to super() and let it reject typos, never swallow them. 3. The two emit_raw_events tests now assert the EFFECT (a piggy-backed raw_event is stripped by _dispatch_event) instead of the attribute value. An attribute assertion cannot fail if the flag stops being honored. 4. The ag-ui-langgraph floor moves to >=0.0.43 (regenerated poetry.lock) so those behavioral tests stop skipping in CI at all. Also drops a dead `graph.get_state = MagicMock()` from the local _make_graph; nothing in the exercised paths reads it (MagicMock auto-creates it anyway). Verification Red-green, fix reverted to the closed 4-kwarg signature: ag-ui-langgraph 0.0.42 -> 1 failed, 4 passed, 2 skipped (test_init_forwards_unknown_kwargs_to_base FAILED — previously this state was fully green, which is the hole being closed) ag-ui-langgraph 0.0.43 -> 6 failed, 1 passed Fix restored, full sdk-python suite: 0.0.43 (Python 3.12 uv venv) -> 237 passed, 11 skipped 0.0.43 (poetry install --with dev, 3.13) -> 237 passed, 11 skipped 0.0.42 (Python 3.12 uv venv) -> 235 passed, 13 skipped The only delta between 0.0.42 and 0.0.43 is the 2 emit_raw_events skips, so the floor bump is the sole thing forcing 0.0.43. `poetry lock` is idempotent on the committed lock (CI runs it before install) and `poetry check --lock` exits 0. `ruff format` leaves the test file unchanged. uv.lock is byte-identical (it locks only the dev group and contains no ag-ui-langgraph entry; CI uses poetry, not uv). Call-Site Enumeration _make_graph (tests/test_agui_agent_clone.py) — CHANGED (get_state line removed). 8 call sites, all in this file (the other test modules each have their own local mock-graph builder). Assumption still holds: the removed line only pre-set an attribute MagicMock creates on demand; both full suites are green. test_init_forwards_unknown_kwargs_to_base, test_init_rejects_unknown_kwarg_ end_to_end — ADDED. No references outside pytest collection. test_init_forwards_upstream_flags, test_clone_carries_upstream_flags — CHANGED bodies only. No references outside pytest collection. BASE_INIT_PARAMS / `import inspect` — UNCHANGED and still live; both skipif markers still read them. Kept deliberately: with the floor at >=0.0.43 they can no longer fire on a supported install, and the version-independent guard above cannot skip regardless, so they only protect a stale local env. `patch`, `EventType`, `TextMessageContentEvent` — ADDED imports, used only in this file. `MagicMock` still used by _make_graph. ag-ui-langgraph constraint (sdk-python/pyproject.toml) — CHANGED. Consumers in-repo: examples/integrations/langgraph-fastapi/agent/pyproject.toml and the langgraph-fastapi / langgraph-python Dockerfiles pin PUBLISHED copilotkit (0.1.93/0.1.94) alongside their own ag-ui-langgraph pins (0.0.41/0.0.37), so they do not resolve this constraint and are unaffected today; when they next bump copilotkit past the release carrying this change their own pins will need to move to >=0.0.43. examples/v1/travel and examples/showcases/* depend on ag-ui-langgraph directly, not through sdk-python. No in-repo target installs copilotkit from this path. sdk-python/poetry.lock — REGENERATED. Read by .github/workflows/ test_unit-python-sdk.yml (`poetry lock && poetry install --with dev`); resolves ag-ui-langgraph 0.0.43. metadata.python-versions stays ">=3.10,<3.15" and uv.lock's requires-python is untouched, so Python 3.10 and 3.11 support is unchanged. Beyond the ag-ui-langgraph bump and its transitive langgraph >=0.6.0 floor (already satisfied by locked 1.1.10), the regeneration also (a) rewrote the generator header 2.1.3 -> 2.3.2, (b) added the previously unlocked [dependency-groups] dev packages (pytest, pytest-asyncio, pluggy, iniconfig, backports-asyncio-runner) and the corresponding main+dev group markers, and (c) normalized a few specifier spellings (2023.03.6 -> 2023.3.6, 14.05.14 -> 14.5.14, 2.0.0b -> 2.0.0b0). All three are artifacts of re-resolving with current Poetry rather than of the dependency change; none alter a resolved main dependency version. sdk-python/pyproject.toml is also read by .github/workflows/ publish-release.yml (build-python fires on any merged PR touching it). Assumption still holds: detect-py-version-changes.sh gates on the version field, which is unchanged at 0.1.95, so the lane reports "Nothing to publish". Publishing this fix still needs a separate version bump. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 17:23:25 +02:00
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.
test(sdk-python): make the clone() kwargs guards falsifiable Three assertions in test_agui_agent_clone.py passed whether or not the code they name was correct. Each was found independently by a reviewer. 1. test_init_rejects_unknown_kwarg_end_to_end used a bare pytest.raises(TypeError). The pre-fix closed 4-parameter signature raises TypeError for a bogus kwarg too, from the subclass frame, so the assertion could not distinguish "forwarded to the base, which rejected it" from "rejected locally before forwarding" — reverting the **kwargs passthrough left it green. Renamed to test_init_rejects_unknown_kwarg_in_the_base_not_locally and given two origin checks: match= on the BASE class's __init__ qualname (a closed signature reports LangGraphAGUIAgent.__init__, not LangGraphAgent.__init__) plus an assertion that the innermost traceback frame is the subclass's own super().__init__ call site rather than the test module's construction call. The match= also fixes the second half of the finding: an unrelated TypeError on this path (the base's clone() re-raises construction failures as TypeError) can no longer satisfy it. 2. test_clone_carries_upstream_flags asserted only the opted-out direction, so it also passed in a world where emit_raw_events was forced False for everyone — the inverse of the opt-out's intent. It now builds a default clone as an in-test control and asserts both directions, rather than borrowing the control from its sibling that the paired skipif silences at the same time. 3. test_clone_resets_per_request_state mutated the source agent in setup and then asserted the clone's attributes are None — true for any instance built through __init__, so the setup was inert. Renamed to test_clone_isolates_per_request_state_from_the_source and given the assertions that make the setup load-bearing: the source keeps its own run-local state across the clone. The two None assertions stay because they are falsifiable (a copy.copy-style clone fails them), not decorative; mutation M9 below proves it. Also in this file: - The 0.0.43 flag names lived in prose in test_clone_succeeds's docstring, referenced by no test body, reading as exhaustive. They are now CLONE_FORWARDED_FLAGS_0_0_43 and are asserted through the base __init__ spy, so the list is live data; the comment says outright that it illustrates the problem rather than staying exhaustive. - test_init_forwards_unknown_kwargs_to_base (renamed test_init_forwards_base_kwargs) now also asserts description and config reach the base. The spy made it a two-line addition, and it closes the gap that nothing pinned two of the four parameters __init__ names. - The TextMessageContentEvent stub was built twice, once inline and once in a nested closure; it is now one module-level _raw_event_message() helper. NOTE: the graph stub is NOT duplicated in this file — _make_graph is already the single local helper and all six call sites use it. The duplication a reviewer flagged is across this file and test_intercepted_tool_call_events.py, which is logged as deferred and untouched here. Both skipif guards are kept deliberately. The declared floor is ag-ui-langgraph >=0.0.42, where emit_raw_events does not exist, so skipping is honest at the floor; test_init_forwards_base_kwargs is the guard that cannot skip on any version. Tests only. No change to copilotkit/langgraph_agui_agent.py, pyproject.toml or any lockfile. Test count is unchanged at 7 in this file. Mutation Evidence Every added or changed assertion was mutation-tested: the guarded behavior was broken, RED confirmed, source restored, GREEN confirmed. Source mutations were applied to copilotkit/langgraph_agui_agent.py; M9/M10 to the installed ag_ui_langgraph/agent.py. Both files restored by checksum afterwards. M1 **kwargs passthrough reverted to the closed 4-parameter signature — the exact scenario the old assertion could not detect. @0.0.42 (the declared floor, what CI installs): NEW file -> 2 failed, 3 passed, 2 skipped (test_init_rejects_unknown_kwarg_in_the_base_not_locally FAILED: "Expected regex: LangGraphAgent\.__init__\(\) got an unexpected keyword argument 'not_a_real_flag_the_base_defines' Actual message: LangGraphAGUIAgent.__init__() got an unexpected keyword argument 'not_a_real_flag_the_base_defines'") PRE-FIX body of the same test -> PASSED. That is the defect. @0.0.43: NEW file -> 7 failed; PRE-FIX bare-raises body -> PASSED. M2 description=None in the super() call -> test_init_forwards_base_kwargs FAILED (1 failed, 6 passed). M3 config=None in the super() call -> test_init_forwards_base_kwargs FAILED. M4 name mangled on the way to the base -> test_init_forwards_base_kwargs FAILED. M5 graph not relayed -> 6 failed, 1 passed. M6 emit_raw_events forced False for everyone -> test_init_forwards_upstream_ flags AND test_clone_carries_upstream_flags FAILED. The PRE-FIX one-sided clone body PASSED under the same mutation. That is defect 2. M7 user's emit_raw_events opt-out overwritten with True -> both flag tests FAILED (the other direction). M8 emit_interrupt_outcome filtered out of the passthrough -> test_init_forwards_base_kwargs FAILED with KeyError. Proves the flag list is live data, not a comment. M9 base clone() replaced with copy.copy(self) -> test_clone_isolates_per_request_state_from_the_source FAILED. Proves the two None assertions are falsifiable and worth keeping. M10 base clone() wipes the SOURCE's run scope -> test_clone_isolates_per_request_state_from_the_source FAILED, while the PRE-FIX tautology body PASSED. That is defect 3. Verification Full sdk-python suite, Python 3.12 uv venv, ruff format --check clean: ag-ui-langgraph 0.0.43 -> 237 passed, 11 skipped ag-ui-langgraph 0.0.42 -> 235 passed, 13 skipped Identical to the pre-change baseline on both versions, so no test count decreased and the 0.0.42 delta is still only the two honest emit_raw_events skips. `ruff check` still reports the same pre-existing I001 it reported on the committed file (no ruff config or ruff step exists for sdk-python), so it is left untouched. git status clean apart from this one test file; neither pyproject.toml nor any lockfile appears in the diff. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 18:01:52 +02:00
"""
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