|
4 | 4 | 1. LangGraph variant: default UUID generation, custom ID passthrough, return value |
5 | 5 | 2. CrewAI variant: default UUID generation, custom ID passthrough, return value |
6 | 6 | 3. AG-UI agent dispatch: custom ID propagates to all three TOOL_CALL events |
| 7 | + 4. AG-UI dispatch validation: defensive CopilotKitMisuseError paths for |
| 8 | + missing/invalid id, name, args, non-serializable args, and non-dict value |
7 | 9 | """ |
8 | 10 |
|
9 | 11 | import json |
@@ -163,6 +165,43 @@ async def test_whitespace_only_id_raises(self): |
163 | 165 | config, name="Tool", args={}, tool_call_id=" " |
164 | 166 | ) |
165 | 167 |
|
| 168 | + @pytest.mark.asyncio |
| 169 | + async def test_whitespace_only_name_raises(self): |
| 170 | + """Passing a whitespace-only name should raise CopilotKitMisuseError.""" |
| 171 | + with patch( |
| 172 | + "copilotkit.langgraph.adispatch_custom_event", new_callable=AsyncMock |
| 173 | + ): |
| 174 | + from copilotkit.langgraph import copilotkit_emit_tool_call |
| 175 | + |
| 176 | + config = {"metadata": {}} |
| 177 | + with pytest.raises(CopilotKitMisuseError, match="non-empty string"): |
| 178 | + await copilotkit_emit_tool_call(config, name=" ", args={}) |
| 179 | + |
| 180 | + @pytest.mark.asyncio |
| 181 | + async def test_empty_name_raises(self): |
| 182 | + """Passing an empty name should raise CopilotKitMisuseError.""" |
| 183 | + with patch( |
| 184 | + "copilotkit.langgraph.adispatch_custom_event", new_callable=AsyncMock |
| 185 | + ): |
| 186 | + from copilotkit.langgraph import copilotkit_emit_tool_call |
| 187 | + |
| 188 | + config = {"metadata": {}} |
| 189 | + with pytest.raises(CopilotKitMisuseError, match="non-empty string"): |
| 190 | + await copilotkit_emit_tool_call(config, name="", args={}) |
| 191 | + |
| 192 | + @pytest.mark.asyncio |
| 193 | + async def test_non_dict_args_raises(self): |
| 194 | + """Passing non-dict args should raise CopilotKitMisuseError.""" |
| 195 | + with patch( |
| 196 | + "copilotkit.langgraph.adispatch_custom_event", new_callable=AsyncMock |
| 197 | + ): |
| 198 | + from copilotkit.langgraph import copilotkit_emit_tool_call |
| 199 | + |
| 200 | + config = {"metadata": {}} |
| 201 | + for bad_args in [None, [1, 2], "raw", 42]: |
| 202 | + with pytest.raises(CopilotKitMisuseError, match="must be a dict"): |
| 203 | + await copilotkit_emit_tool_call(config, name="Tool", args=bad_args) |
| 204 | + |
166 | 205 |
|
167 | 206 | # ---- CrewAI variant tests ---- |
168 | 207 |
|
@@ -254,6 +293,39 @@ async def test_whitespace_only_id_raises(self): |
254 | 293 | name="Tool", args={}, tool_call_id=" " |
255 | 294 | ) |
256 | 295 |
|
| 296 | + @pytest.mark.asyncio |
| 297 | + async def test_none_id_generates_uuid(self): |
| 298 | + """Explicitly passing tool_call_id=None should behave the same as omitting it.""" |
| 299 | + with patch( |
| 300 | + "copilotkit.crewai.crewai_sdk.queue_put", new_callable=AsyncMock |
| 301 | + ): |
| 302 | + from copilotkit.crewai.crewai_sdk import copilotkit_emit_tool_call |
| 303 | + |
| 304 | + result = await copilotkit_emit_tool_call( |
| 305 | + name="Tool", args={}, tool_call_id=None |
| 306 | + ) |
| 307 | + assert isinstance(result, str) |
| 308 | + uuid.UUID(result) |
| 309 | + |
| 310 | + @pytest.mark.asyncio |
| 311 | + async def test_whitespace_only_name_raises(self): |
| 312 | + """Passing a whitespace-only name should raise CopilotKitMisuseError.""" |
| 313 | + with patch("copilotkit.crewai.crewai_sdk.queue_put", new_callable=AsyncMock): |
| 314 | + from copilotkit.crewai.crewai_sdk import copilotkit_emit_tool_call |
| 315 | + |
| 316 | + with pytest.raises(CopilotKitMisuseError, match="non-empty string"): |
| 317 | + await copilotkit_emit_tool_call(name=" ", args={}) |
| 318 | + |
| 319 | + @pytest.mark.asyncio |
| 320 | + async def test_non_dict_args_raises(self): |
| 321 | + """Passing non-dict args should raise CopilotKitMisuseError.""" |
| 322 | + with patch("copilotkit.crewai.crewai_sdk.queue_put", new_callable=AsyncMock): |
| 323 | + from copilotkit.crewai.crewai_sdk import copilotkit_emit_tool_call |
| 324 | + |
| 325 | + for bad_args in [None, [1, 2], "raw", 42]: |
| 326 | + with pytest.raises(CopilotKitMisuseError, match="must be a dict"): |
| 327 | + await copilotkit_emit_tool_call(name="Tool", args=bad_args) |
| 328 | + |
257 | 329 |
|
258 | 330 | # ---- AG-UI dispatch: custom ID propagates through all events ---- |
259 | 331 |
|
@@ -426,15 +498,45 @@ def test_missing_args_raises(self, agent): |
426 | 498 | name=CustomEventNames.ManuallyEmitToolCall.value, |
427 | 499 | value={"id": "valid-id", "name": "Tool"}, |
428 | 500 | ) |
429 | | - with pytest.raises(CopilotKitMisuseError, match="missing 'args'"): |
| 501 | + with pytest.raises(CopilotKitMisuseError, match="must be a dict or pre-serialized"): |
430 | 502 | agent._dispatch_event(event) |
431 | 503 |
|
432 | 504 | def test_non_serializable_args_raises(self, agent): |
433 | | - """Event with non-JSON-serializable args should raise CopilotKitMisuseError.""" |
| 505 | + """Event with non-JSON-serializable args (set) should raise CopilotKitMisuseError.""" |
434 | 506 | event = CustomEvent( |
435 | 507 | type=EventType.CUSTOM, |
436 | 508 | name=CustomEventNames.ManuallyEmitToolCall.value, |
437 | 509 | value={"id": "valid-id", "name": "Tool", "args": {1, 2, 3}}, |
438 | 510 | ) |
439 | | - with pytest.raises(CopilotKitMisuseError, match="not JSON-serializable"): |
| 511 | + with pytest.raises(CopilotKitMisuseError, match="must be a dict or pre-serialized"): |
| 512 | + agent._dispatch_event(event) |
| 513 | + |
| 514 | + def test_non_dict_value_raises(self, agent): |
| 515 | + """Event with non-dict value should raise CopilotKitMisuseError.""" |
| 516 | + event = CustomEvent( |
| 517 | + type=EventType.CUSTOM, |
| 518 | + name=CustomEventNames.ManuallyEmitToolCall.value, |
| 519 | + value=None, |
| 520 | + ) |
| 521 | + with pytest.raises(CopilotKitMisuseError, match="must be a dict"): |
| 522 | + agent._dispatch_event(event) |
| 523 | + |
| 524 | + def test_list_args_raises(self, agent): |
| 525 | + """Event with list args should raise CopilotKitMisuseError.""" |
| 526 | + event = CustomEvent( |
| 527 | + type=EventType.CUSTOM, |
| 528 | + name=CustomEventNames.ManuallyEmitToolCall.value, |
| 529 | + value={"id": "valid-id", "name": "Tool", "args": [1, 2, 3]}, |
| 530 | + ) |
| 531 | + with pytest.raises(CopilotKitMisuseError, match="must be a dict or pre-serialized"): |
| 532 | + agent._dispatch_event(event) |
| 533 | + |
| 534 | + def test_int_args_raises(self, agent): |
| 535 | + """Event with int args should raise CopilotKitMisuseError.""" |
| 536 | + event = CustomEvent( |
| 537 | + type=EventType.CUSTOM, |
| 538 | + name=CustomEventNames.ManuallyEmitToolCall.value, |
| 539 | + value={"id": "valid-id", "name": "Tool", "args": 42}, |
| 540 | + ) |
| 541 | + with pytest.raises(CopilotKitMisuseError, match="must be a dict or pre-serialized"): |
440 | 542 | agent._dispatch_event(event) |
0 commit comments