99from click .testing import CliRunner
1010
1111from airbyte_cdk .cli .airbyte_cdk ._manifest import manifest_cli_group
12+ from airbyte_cdk .sources .declarative .parsers .manifest_normalizer import ManifestNormalizer
1213
1314
1415class TestManifestValidateCommand :
@@ -373,6 +374,126 @@ def test_migrate_manifest_not_dict(self, tmp_path: Path) -> None:
373374 assert "does not contain a valid YAML dictionary" in result .output
374375
375376
377+ class TestManifestNormalizeCommand :
378+ """Test cases for the manifest normalize command."""
379+
380+ def test_normalize_valid_manifest_no_changes (self , tmp_path : Path ) -> None :
381+ """Test normalize command with a manifest that doesn't need normalization."""
382+ manifest_content = {
383+ "version" : "0.29.0" ,
384+ "type" : "DeclarativeSource" ,
385+ "check" : {"type" : "CheckStream" , "stream_names" : ["users" ]},
386+ "streams" : [
387+ {
388+ "type" : "DeclarativeStream" ,
389+ "name" : "users" ,
390+ "primary_key" : [],
391+ "retriever" : {
392+ "type" : "SimpleRetriever" ,
393+ "requester" : {
394+ "type" : "HttpRequester" ,
395+ "url_base" : "https://api.example.com" ,
396+ "path" : "/users" ,
397+ },
398+ "record_selector" : {
399+ "type" : "RecordSelector" ,
400+ "extractor" : {"type" : "DpathExtractor" , "field_path" : []},
401+ },
402+ },
403+ }
404+ ],
405+ }
406+
407+ manifest_file = tmp_path / "manifest.yaml"
408+ with open (manifest_file , "w" ) as f :
409+ yaml .dump (manifest_content , f )
410+
411+ runner = CliRunner ()
412+ result = runner .invoke (
413+ manifest_cli_group , ["normalize" , "--manifest-path" , str (manifest_file )]
414+ )
415+
416+ assert result .exit_code == 0
417+ assert "✅ Manifest" in result .output
418+ assert "is already normalized" in result .output
419+
420+ def test_normalize_manifest_dry_run (self , tmp_path : Path ) -> None :
421+ """Test normalize command with dry-run flag."""
422+ manifest_content = {
423+ "version" : "0.29.0" ,
424+ "type" : "DeclarativeSource" ,
425+ "check" : {"type" : "CheckStream" , "stream_names" : ["users" ]},
426+ "streams" : [
427+ {
428+ "type" : "DeclarativeStream" ,
429+ "name" : "users" ,
430+ "primary_key" : [],
431+ "retriever" : {
432+ "type" : "SimpleRetriever" ,
433+ "requester" : {
434+ "type" : "HttpRequester" ,
435+ "url_base" : "https://api.example.com" ,
436+ "path" : "/users" ,
437+ },
438+ "record_selector" : {
439+ "type" : "RecordSelector" ,
440+ "extractor" : {"type" : "DpathExtractor" , "field_path" : []},
441+ },
442+ },
443+ }
444+ ],
445+ }
446+
447+ manifest_file = tmp_path / "manifest.yaml"
448+ with open (manifest_file , "w" ) as f :
449+ yaml .dump (manifest_content , f )
450+
451+ runner = CliRunner ()
452+ result = runner .invoke (
453+ manifest_cli_group , ["normalize" , "--manifest-path" , str (manifest_file ), "--dry-run" ]
454+ )
455+
456+ assert result .exit_code == 0
457+ assert ("🔍 Dry run" in result .output ) or ("is already normalized" in result .output )
458+
459+ def test_normalize_manifest_file_not_found (self ) -> None :
460+ """Test normalize command with non-existent manifest file."""
461+ runner = CliRunner ()
462+ result = runner .invoke (
463+ manifest_cli_group , ["normalize" , "--manifest-path" , "nonexistent.yaml" ]
464+ )
465+
466+ assert result .exit_code == 2
467+ assert "--manifest-path" in result .output
468+ assert "does not exist" in result .output
469+
470+ def test_normalize_invalid_yaml (self , tmp_path : Path ) -> None :
471+ """Test normalize command with invalid YAML file."""
472+ manifest_file = tmp_path / "invalid.yaml"
473+ manifest_file .write_text ("invalid: yaml: content: [" )
474+
475+ runner = CliRunner ()
476+ result = runner .invoke (
477+ manifest_cli_group , ["normalize" , "--manifest-path" , str (manifest_file )]
478+ )
479+
480+ assert result .exit_code == 3
481+ assert "❌ Error: Invalid YAML" in result .output
482+
483+ def test_normalize_non_dict_yaml (self , tmp_path : Path ) -> None :
484+ """Test normalize command with YAML that's not a dictionary."""
485+ manifest_file = tmp_path / "list.yaml"
486+ manifest_file .write_text ("- item1\n - item2" )
487+
488+ runner = CliRunner ()
489+ result = runner .invoke (
490+ manifest_cli_group , ["normalize" , "--manifest-path" , str (manifest_file )]
491+ )
492+
493+ assert result .exit_code == 3
494+ assert "does not contain a valid YAML dictionary" in result .output
495+
496+
376497class TestManifestCliHelp :
377498 """Test cases for CLI help text and command structure."""
378499
@@ -385,6 +506,7 @@ def test_manifest_group_help(self) -> None:
385506 assert "Manifest related commands" in result .output
386507 assert "validate" in result .output
387508 assert "migrate" in result .output
509+ assert "normalize" in result .output
388510
389511 def test_validate_command_help (self ) -> None :
390512 """Test that the validate command shows help with exit codes."""
@@ -407,3 +529,13 @@ def test_migrate_command_help(self) -> None:
407529 assert "Apply migrations" in result .output
408530 assert "--dry-run" in result .output
409531 assert "--manifest-path" in result .output
532+
533+ def test_normalize_command_help (self ) -> None :
534+ """Test that normalize command help text is displayed correctly."""
535+ runner = CliRunner ()
536+ result = runner .invoke (manifest_cli_group , ["normalize" , "--help" ])
537+
538+ assert result .exit_code == 0
539+ assert "Normalize a manifest file by removing duplicated definitions" in result .output
540+ assert "--manifest-path" in result .output
541+ assert "--dry-run" in result .output
0 commit comments