@@ -453,6 +453,52 @@ fn test_python_config_warns_not_functional() {
453453 assert ! ( warnings. iter( ) . any( |w| w. contains( "will not be available" ) ) ) ;
454454}
455455
456+ #[ test]
457+ fn test_python_validate_bash_disabled_warning ( ) {
458+ let ( fm, _) =
459+ parse_markdown ( "---\n name: test\n description: test\n tools:\n bash: []\n ---\n " ) . unwrap ( ) ;
460+ let ext = crate :: runtimes:: python:: PythonExtension :: new (
461+ crate :: runtimes:: python:: PythonRuntimeConfig :: Enabled ( true ) ,
462+ ) ;
463+ let ctx = ctx_from ( & fm) ;
464+ let warnings = ext. validate ( & ctx) . unwrap ( ) ;
465+ assert ! ( !warnings. is_empty( ) ) ;
466+ assert ! ( warnings[ 0 ] . contains( "tools.bash is empty" ) ) ;
467+ }
468+
469+ #[ test]
470+ fn test_python_validate_bash_not_disabled_no_warning ( ) {
471+ let fm = minimal_front_matter ( ) ;
472+ let ext = crate :: runtimes:: python:: PythonExtension :: new (
473+ crate :: runtimes:: python:: PythonRuntimeConfig :: Enabled ( true ) ,
474+ ) ;
475+ let ctx = ctx_from ( & fm) ;
476+ let warnings = ext. validate ( & ctx) . unwrap ( ) ;
477+ assert ! ( warnings. is_empty( ) ) ;
478+ }
479+
480+ #[ test]
481+ fn test_python_invalid_feed_url_rejected ( ) {
482+ let ( fm, _) = parse_markdown (
483+ "---\n name: test\n description: test\n runtimes:\n python:\n feed-url: 'pkgs.dev.azure.com/no-scheme'\n ---\n " ,
484+ ) . unwrap ( ) ;
485+ let python = fm. runtimes . as_ref ( ) . unwrap ( ) . python . as_ref ( ) . unwrap ( ) ;
486+ let ext = crate :: runtimes:: python:: PythonExtension :: new ( python. clone ( ) ) ;
487+ let ctx = ctx_from ( & fm) ;
488+ assert ! ( ext. validate( & ctx) . is_err( ) ) ;
489+ }
490+
491+ #[ test]
492+ fn test_python_validate_version_injection_rejected ( ) {
493+ let ( fm, _) = parse_markdown (
494+ "---\n name: test\n description: test\n runtimes:\n python:\n version: '$(SECRET)'\n ---\n " ,
495+ ) . unwrap ( ) ;
496+ let python = fm. runtimes . as_ref ( ) . unwrap ( ) . python . as_ref ( ) . unwrap ( ) ;
497+ let ext = crate :: runtimes:: python:: PythonExtension :: new ( python. clone ( ) ) ;
498+ let ctx = ctx_from ( & fm) ;
499+ assert ! ( ext. validate( & ctx) . is_err( ) ) ;
500+ }
501+
456502// ── NodeExtension ──────────────────────────────────────────────
457503
458504#[ test]
@@ -562,6 +608,41 @@ fn test_node_config_and_feed_url_mutually_exclusive() {
562608 assert ! ( result. unwrap_err( ) . to_string( ) . contains( "mutually exclusive" ) ) ;
563609}
564610
611+ #[ test]
612+ fn test_node_validate_bash_disabled_warning ( ) {
613+ let ( fm, _) =
614+ parse_markdown ( "---\n name: test\n description: test\n tools:\n bash: []\n ---\n " ) . unwrap ( ) ;
615+ let ext = crate :: runtimes:: node:: NodeExtension :: new (
616+ crate :: runtimes:: node:: NodeRuntimeConfig :: Enabled ( true ) ,
617+ ) ;
618+ let ctx = ctx_from ( & fm) ;
619+ let warnings = ext. validate ( & ctx) . unwrap ( ) ;
620+ assert ! ( !warnings. is_empty( ) ) ;
621+ assert ! ( warnings[ 0 ] . contains( "tools.bash is empty" ) ) ;
622+ }
623+
624+ #[ test]
625+ fn test_node_invalid_feed_url_rejected ( ) {
626+ let ( fm, _) = parse_markdown (
627+ "---\n name: test\n description: test\n runtimes:\n node:\n feed-url: 'pkgs.dev.azure.com/no-scheme'\n ---\n " ,
628+ ) . unwrap ( ) ;
629+ let node = fm. runtimes . as_ref ( ) . unwrap ( ) . node . as_ref ( ) . unwrap ( ) ;
630+ let ext = crate :: runtimes:: node:: NodeExtension :: new ( node. clone ( ) ) ;
631+ let ctx = ctx_from ( & fm) ;
632+ assert ! ( ext. validate( & ctx) . is_err( ) ) ;
633+ }
634+
635+ #[ test]
636+ fn test_node_validate_version_injection_rejected ( ) {
637+ let ( fm, _) = parse_markdown (
638+ "---\n name: test\n description: test\n runtimes:\n node:\n version: '$(SECRET)'\n ---\n " ,
639+ ) . unwrap ( ) ;
640+ let node = fm. runtimes . as_ref ( ) . unwrap ( ) . node . as_ref ( ) . unwrap ( ) ;
641+ let ext = crate :: runtimes:: node:: NodeExtension :: new ( node. clone ( ) ) ;
642+ let ctx = ctx_from ( & fm) ;
643+ assert ! ( ext. validate( & ctx) . is_err( ) ) ;
644+ }
645+
565646#[ test]
566647fn test_python_config_and_feed_url_mutually_exclusive ( ) {
567648 let ( fm, _) = parse_markdown (
@@ -795,6 +876,41 @@ fn test_dotnet_no_version_with_global_json_present_ok() {
795876 assert ! ( ext. validate( & ctx) . is_ok( ) ) ;
796877}
797878
879+ #[ test]
880+ fn test_dotnet_validate_bash_disabled_warning ( ) {
881+ let ( fm, _) =
882+ parse_markdown ( "---\n name: test\n description: test\n tools:\n bash: []\n ---\n " ) . unwrap ( ) ;
883+ let ext = crate :: runtimes:: dotnet:: DotnetExtension :: new (
884+ crate :: runtimes:: dotnet:: DotnetRuntimeConfig :: Enabled ( true ) ,
885+ ) ;
886+ let ctx = ctx_from ( & fm) ;
887+ let warnings = ext. validate ( & ctx) . unwrap ( ) ;
888+ assert ! ( !warnings. is_empty( ) ) ;
889+ assert ! ( warnings[ 0 ] . contains( "tools.bash is empty" ) ) ;
890+ }
891+
892+ #[ test]
893+ fn test_dotnet_validate_version_injection_rejected ( ) {
894+ let ( fm, _) = parse_markdown (
895+ "---\n name: test\n description: test\n runtimes:\n dotnet:\n version: '$(SECRET)'\n ---\n " ,
896+ ) . unwrap ( ) ;
897+ let dotnet = fm. runtimes . as_ref ( ) . unwrap ( ) . dotnet . as_ref ( ) . unwrap ( ) ;
898+ let ext = crate :: runtimes:: dotnet:: DotnetExtension :: new ( dotnet. clone ( ) ) ;
899+ let ctx = ctx_from ( & fm) ;
900+ assert ! ( ext. validate( & ctx) . is_err( ) ) ;
901+ }
902+
903+ #[ test]
904+ fn test_dotnet_validate_config_injection_rejected ( ) {
905+ let ( fm, _) = parse_markdown (
906+ "---\n name: test\n description: test\n runtimes:\n dotnet:\n config: '$(SECRET)/nuget.config'\n ---\n " ,
907+ ) . unwrap ( ) ;
908+ let dotnet = fm. runtimes . as_ref ( ) . unwrap ( ) . dotnet . as_ref ( ) . unwrap ( ) ;
909+ let ext = crate :: runtimes:: dotnet:: DotnetExtension :: new ( dotnet. clone ( ) ) ;
910+ let ctx = ctx_from ( & fm) ;
911+ assert ! ( ext. validate( & ctx) . is_err( ) ) ;
912+ }
913+
798914// ── Multiple runtimes ──────────────────────────────────────────
799915
800916#[ test]
0 commit comments