Skip to content

Commit ee7080c

Browse files
authored
test(runtimes): add validate() unit tests and feed-url integration tests for all runtime extensions (#509)
1 parent 27b6cd7 commit ee7080c

2 files changed

Lines changed: 240 additions & 0 deletions

File tree

src/compile/extensions/tests.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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("---\nname: test\ndescription: test\ntools:\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+
"---\nname: test\ndescription: test\nruntimes:\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+
"---\nname: test\ndescription: test\nruntimes:\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("---\nname: test\ndescription: test\ntools:\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+
"---\nname: test\ndescription: test\nruntimes:\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+
"---\nname: test\ndescription: test\nruntimes:\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]
566647
fn 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("---\nname: test\ndescription: test\ntools:\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+
"---\nname: test\ndescription: test\nruntimes:\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+
"---\nname: test\ndescription: test\nruntimes:\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]

tests/compiler_tests.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3112,6 +3112,130 @@ safe-outputs:
31123112
let _ = fs::remove_dir_all(&temp_dir);
31133113
}
31143114

3115+
/// Integration test: `runtimes: python:` with `feed-url:` end-to-end compilation
3116+
///
3117+
/// Verifies that when `feed-url` is set, the compiler emits `PipAuthenticate@1`
3118+
/// and injects `PIP_INDEX_URL` / `UV_DEFAULT_INDEX` env vars into the agent step.
3119+
#[test]
3120+
fn test_python_runtime_with_feed_url_compiled_output() {
3121+
let temp_dir = std::env::temp_dir().join(format!(
3122+
"agentic-pipeline-python-feed-{}",
3123+
std::process::id()
3124+
));
3125+
fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
3126+
3127+
let input = r#"---
3128+
name: "Python Feed Agent"
3129+
description: "Python agent with internal feed"
3130+
runtimes:
3131+
python:
3132+
feed-url: "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/pypi/simple/"
3133+
safe-outputs:
3134+
noop: {}
3135+
---
3136+
3137+
## Python Feed Agent
3138+
"#;
3139+
3140+
let input_path = temp_dir.join("python-feed-agent.md");
3141+
let output_path = temp_dir.join("python-feed-agent.yml");
3142+
fs::write(&input_path, input).expect("Failed to write test input");
3143+
3144+
let binary_path = PathBuf::from(env!("CARGO_BIN_EXE_ado-aw"));
3145+
let output = std::process::Command::new(&binary_path)
3146+
.args([
3147+
"compile",
3148+
input_path.to_str().unwrap(),
3149+
"-o",
3150+
output_path.to_str().unwrap(),
3151+
])
3152+
.output()
3153+
.expect("Failed to run compiler");
3154+
3155+
assert!(
3156+
output.status.success(),
3157+
"Compiler should succeed: {}",
3158+
String::from_utf8_lossy(&output.stderr)
3159+
);
3160+
3161+
let compiled = fs::read_to_string(&output_path).expect("Should read compiled YAML");
3162+
3163+
assert!(
3164+
compiled.contains("PipAuthenticate@1"),
3165+
"Should include PipAuthenticate@1 when feed-url is set"
3166+
);
3167+
assert!(
3168+
compiled.contains("PIP_INDEX_URL"),
3169+
"Should inject PIP_INDEX_URL env var when feed-url is set"
3170+
);
3171+
assert!(
3172+
compiled.contains("UV_DEFAULT_INDEX"),
3173+
"Should inject UV_DEFAULT_INDEX env var when feed-url is set"
3174+
);
3175+
3176+
let _ = fs::remove_dir_all(&temp_dir);
3177+
}
3178+
3179+
/// Integration test: `runtimes: node:` with `feed-url:` end-to-end compilation
3180+
///
3181+
/// Verifies that when `feed-url` is set, the compiler emits `npmAuthenticate@0`
3182+
/// and injects `NPM_CONFIG_REGISTRY` env var into the agent step.
3183+
#[test]
3184+
fn test_node_runtime_with_feed_url_compiled_output() {
3185+
let temp_dir = std::env::temp_dir().join(format!(
3186+
"agentic-pipeline-node-feed-{}",
3187+
std::process::id()
3188+
));
3189+
fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
3190+
3191+
let input = r#"---
3192+
name: "Node Feed Agent"
3193+
description: "Node agent with internal npm feed"
3194+
runtimes:
3195+
node:
3196+
feed-url: "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/npm/registry/"
3197+
safe-outputs:
3198+
noop: {}
3199+
---
3200+
3201+
## Node Feed Agent
3202+
"#;
3203+
3204+
let input_path = temp_dir.join("node-feed-agent.md");
3205+
let output_path = temp_dir.join("node-feed-agent.yml");
3206+
fs::write(&input_path, input).expect("Failed to write test input");
3207+
3208+
let binary_path = PathBuf::from(env!("CARGO_BIN_EXE_ado-aw"));
3209+
let output = std::process::Command::new(&binary_path)
3210+
.args([
3211+
"compile",
3212+
input_path.to_str().unwrap(),
3213+
"-o",
3214+
output_path.to_str().unwrap(),
3215+
])
3216+
.output()
3217+
.expect("Failed to run compiler");
3218+
3219+
assert!(
3220+
output.status.success(),
3221+
"Compiler should succeed: {}",
3222+
String::from_utf8_lossy(&output.stderr)
3223+
);
3224+
3225+
let compiled = fs::read_to_string(&output_path).expect("Should read compiled YAML");
3226+
3227+
assert!(
3228+
compiled.contains("npmAuthenticate@0"),
3229+
"Should include npmAuthenticate@0 when feed-url is set"
3230+
);
3231+
assert!(
3232+
compiled.contains("NPM_CONFIG_REGISTRY"),
3233+
"Should inject NPM_CONFIG_REGISTRY env var when feed-url is set"
3234+
);
3235+
3236+
let _ = fs::remove_dir_all(&temp_dir);
3237+
}
3238+
31153239
/// Integration test: `schedule:` object form with `branches:` end-to-end compilation
31163240
///
31173241
/// Verifies that a pipeline compiled with the object-form schedule containing

0 commit comments

Comments
 (0)