|
1 | 1 | import uuid |
| 2 | + |
2 | 3 | from typing import Any |
3 | 4 | from unittest.mock import patch |
4 | 5 |
|
5 | 6 | import pytest |
6 | | -from google.protobuf.json_format import MessageToDict |
7 | 7 |
|
8 | | -from a2a.types.a2a_pb2 import ( |
9 | | - Artifact, |
| 8 | +from a2a.types import ( |
| 9 | + AgentCapabilities, |
10 | 10 | AgentCard, |
11 | 11 | AgentCardSignature, |
12 | | - AgentCapabilities, |
13 | 12 | AgentInterface, |
14 | 13 | AgentSkill, |
| 14 | + Artifact, |
15 | 15 | Message, |
16 | 16 | Part, |
17 | 17 | Role, |
|
23 | 23 | ) |
24 | 24 | from a2a.utils.errors import ServerError |
25 | 25 | from a2a.utils.helpers import ( |
| 26 | + _clean_empty, |
26 | 27 | append_artifact_to_task, |
27 | 28 | are_modalities_compatible, |
28 | 29 | build_text_artifact, |
| 30 | + canonicalize_agent_card, |
29 | 31 | create_task_obj, |
30 | 32 | validate, |
31 | | - canonicalize_agent_card, |
32 | 33 | ) |
33 | 34 |
|
34 | 35 |
|
@@ -392,3 +393,98 @@ def test_canonicalize_agent_card(): |
392 | 393 | ) |
393 | 394 | result = canonicalize_agent_card(agent_card) |
394 | 395 | assert result == expected_jcs |
| 396 | + |
| 397 | + |
| 398 | +def test_canonicalize_agent_card_preserves_false_capability(): |
| 399 | + """Regression #692: streaming=False must not be stripped from canonical JSON.""" |
| 400 | + card = AgentCard( |
| 401 | + **{ |
| 402 | + **SAMPLE_AGENT_CARD, |
| 403 | + 'capabilities': AgentCapabilities( |
| 404 | + streaming=False, |
| 405 | + push_notifications=True, |
| 406 | + ), |
| 407 | + } |
| 408 | + ) |
| 409 | + result = canonicalize_agent_card(card) |
| 410 | + assert '"streaming":false' in result |
| 411 | + |
| 412 | + |
| 413 | +@pytest.mark.parametrize( |
| 414 | + 'input_val', |
| 415 | + [ |
| 416 | + pytest.param({'a': ''}, id='empty-string'), |
| 417 | + pytest.param({'a': []}, id='empty-list'), |
| 418 | + pytest.param({'a': {}}, id='empty-dict'), |
| 419 | + pytest.param({'a': {'b': []}}, id='nested-empty'), |
| 420 | + pytest.param({'a': '', 'b': [], 'c': {}}, id='all-empties'), |
| 421 | + pytest.param({'a': {'b': {'c': ''}}}, id='deeply-nested'), |
| 422 | + ], |
| 423 | +) |
| 424 | +def test_clean_empty_removes_empties(input_val): |
| 425 | + """_clean_empty removes empty strings, lists, and dicts recursively.""" |
| 426 | + assert _clean_empty(input_val) is None |
| 427 | + |
| 428 | + |
| 429 | +def test_clean_empty_top_level_list_becomes_none(): |
| 430 | + """Top-level list that becomes empty after cleaning should return None.""" |
| 431 | + assert _clean_empty(['', {}, []]) is None |
| 432 | + |
| 433 | + |
| 434 | +@pytest.mark.parametrize( |
| 435 | + 'input_val,expected', |
| 436 | + [ |
| 437 | + pytest.param({'retries': 0}, {'retries': 0}, id='int-zero'), |
| 438 | + pytest.param({'enabled': False}, {'enabled': False}, id='bool-false'), |
| 439 | + pytest.param({'score': 0.0}, {'score': 0.0}, id='float-zero'), |
| 440 | + pytest.param([0, 1, 2], [0, 1, 2], id='zero-in-list'), |
| 441 | + pytest.param([False, True], [False, True], id='false-in-list'), |
| 442 | + pytest.param( |
| 443 | + {'config': {'max_retries': 0, 'name': 'agent'}}, |
| 444 | + {'config': {'max_retries': 0, 'name': 'agent'}}, |
| 445 | + id='nested-zero', |
| 446 | + ), |
| 447 | + ], |
| 448 | +) |
| 449 | +def test_clean_empty_preserves_falsy_values(input_val, expected): |
| 450 | + """_clean_empty preserves legitimate falsy values (0, False, 0.0).""" |
| 451 | + assert _clean_empty(input_val) == expected |
| 452 | + |
| 453 | + |
| 454 | +@pytest.mark.parametrize( |
| 455 | + 'input_val,expected', |
| 456 | + [ |
| 457 | + pytest.param( |
| 458 | + {'count': 0, 'label': '', 'items': []}, |
| 459 | + {'count': 0}, |
| 460 | + id='falsy-with-empties', |
| 461 | + ), |
| 462 | + pytest.param( |
| 463 | + {'a': 0, 'b': 'hello', 'c': False, 'd': ''}, |
| 464 | + {'a': 0, 'b': 'hello', 'c': False}, |
| 465 | + id='mixed-types', |
| 466 | + ), |
| 467 | + pytest.param( |
| 468 | + {'name': 'agent', 'retries': 0, 'tags': [], 'desc': ''}, |
| 469 | + {'name': 'agent', 'retries': 0}, |
| 470 | + id='realistic-mixed', |
| 471 | + ), |
| 472 | + ], |
| 473 | +) |
| 474 | +def test_clean_empty_mixed(input_val, expected): |
| 475 | + """_clean_empty handles mixed empty and falsy values correctly.""" |
| 476 | + assert _clean_empty(input_val) == expected |
| 477 | + |
| 478 | + |
| 479 | +def test_clean_empty_does_not_mutate_input(): |
| 480 | + """_clean_empty should not mutate the original input object.""" |
| 481 | + original = {'a': '', 'b': 1, 'c': {'d': ''}} |
| 482 | + original_copy = { |
| 483 | + 'a': '', |
| 484 | + 'b': 1, |
| 485 | + 'c': {'d': ''}, |
| 486 | + } |
| 487 | + |
| 488 | + _clean_empty(original) |
| 489 | + |
| 490 | + assert original == original_copy |
0 commit comments