2727 build_text_artifact ,
2828 create_task_obj ,
2929 validate ,
30+ _clean_empty ,
3031 canonicalize_agent_card ,
3132)
3233
@@ -380,3 +381,96 @@ def test_canonicalize_agent_card():
380381 )
381382 result = canonicalize_agent_card (agent_card )
382383 assert result == expected_jcs
384+
385+
386+ def test_canonicalize_agent_card_preserves_false_capability ():
387+ """Regression #692: streaming=False must not be stripped from canonical JSON."""
388+ card = AgentCard (** {
389+ ** SAMPLE_AGENT_CARD ,
390+ 'capabilities' : AgentCapabilities (
391+ streaming = False ,
392+ push_notifications = True ,
393+ ),
394+ })
395+ result = canonicalize_agent_card (card )
396+ assert '"streaming":false' in result
397+
398+
399+ @pytest .mark .parametrize (
400+ 'input_val' ,
401+ [
402+ pytest .param ({'a' : '' }, id = 'empty-string' ),
403+ pytest .param ({'a' : []}, id = 'empty-list' ),
404+ pytest .param ({'a' : {}}, id = 'empty-dict' ),
405+ pytest .param ({'a' : {'b' : []}}, id = 'nested-empty' ),
406+ pytest .param ({'a' : '' , 'b' : [], 'c' : {}}, id = 'all-empties' ),
407+ pytest .param ({'a' : {'b' : {'c' : '' }}}, id = 'deeply-nested' ),
408+ ],
409+ )
410+ def test_clean_empty_removes_empties (input_val ):
411+ """_clean_empty removes empty strings, lists, and dicts recursively."""
412+ assert _clean_empty (input_val ) is None
413+
414+
415+ def test_clean_empty_top_level_list_becomes_none ():
416+ """Top-level list that becomes empty after cleaning should return None."""
417+ assert _clean_empty (['' , {}, []]) is None
418+
419+
420+ @pytest .mark .parametrize (
421+ 'input_val,expected' ,
422+ [
423+ pytest .param ({'retries' : 0 }, {'retries' : 0 }, id = 'int-zero' ),
424+ pytest .param ({'enabled' : False }, {'enabled' : False }, id = 'bool-false' ),
425+ pytest .param ({'score' : 0.0 }, {'score' : 0.0 }, id = 'float-zero' ),
426+ pytest .param ([0 , 1 , 2 ], [0 , 1 , 2 ], id = 'zero-in-list' ),
427+ pytest .param ([False , True ], [False , True ], id = 'false-in-list' ),
428+ pytest .param (
429+ {'config' : {'max_retries' : 0 , 'name' : 'agent' }},
430+ {'config' : {'max_retries' : 0 , 'name' : 'agent' }},
431+ id = 'nested-zero' ,
432+ ),
433+ ],
434+ )
435+ def test_clean_empty_preserves_falsy_values (input_val , expected ):
436+ """_clean_empty preserves legitimate falsy values (0, False, 0.0)."""
437+ assert _clean_empty (input_val ) == expected
438+
439+
440+ @pytest .mark .parametrize (
441+ 'input_val,expected' ,
442+ [
443+ pytest .param (
444+ {'count' : 0 , 'label' : '' , 'items' : []},
445+ {'count' : 0 },
446+ id = 'falsy-with-empties' ,
447+ ),
448+ pytest .param (
449+ {'a' : 0 , 'b' : 'hello' , 'c' : False , 'd' : '' },
450+ {'a' : 0 , 'b' : 'hello' , 'c' : False },
451+ id = 'mixed-types' ,
452+ ),
453+ pytest .param (
454+ {'name' : 'agent' , 'retries' : 0 , 'tags' : [], 'desc' : '' },
455+ {'name' : 'agent' , 'retries' : 0 },
456+ id = 'realistic-mixed' ,
457+ ),
458+ ],
459+ )
460+ def test_clean_empty_mixed (input_val , expected ):
461+ """_clean_empty handles mixed empty and falsy values correctly."""
462+ assert _clean_empty (input_val ) == expected
463+
464+
465+ def test_clean_empty_does_not_mutate_input ():
466+ """_clean_empty should not mutate the original input object."""
467+ original = {'a' : '' , 'b' : 1 , 'c' : {'d' : '' }}
468+ copy = {
469+ 'a' : '' ,
470+ 'b' : 1 ,
471+ 'c' : {'d' : '' },
472+ }
473+
474+ _clean_empty (original )
475+
476+ assert original == copy
0 commit comments