Skip to content

Commit df4d120

Browse files
test: add coverage for Python/Node runtimes and upload-pipeline-artifact (#461)
* Initial plan * test: add coverage for Python/Node runtimes, upload-pipeline-artifact, fix Node display_name Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/fe597b1f-5968-4257-8d4c-1b63e8dfc7e4 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
1 parent 6c1a332 commit df4d120

7 files changed

Lines changed: 301 additions & 5 deletions

File tree

src/compile/extensions/tests.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ fn test_collect_extensions_node_enabled() {
461461
parse_markdown("---\nname: test\ndescription: test\nruntimes:\n node: true\n---\n")
462462
.unwrap();
463463
let exts = collect_extensions(&fm);
464-
assert!(exts.iter().any(|e| e.name() == "Node.js"));
464+
assert!(exts.iter().any(|e| e.name() == "Node"));
465465
}
466466

467467
#[test]
@@ -470,7 +470,7 @@ fn test_collect_extensions_node_disabled() {
470470
parse_markdown("---\nname: test\ndescription: test\nruntimes:\n node: false\n---\n")
471471
.unwrap();
472472
let exts = collect_extensions(&fm);
473-
assert!(!exts.iter().any(|e| e.name() == "Node.js"));
473+
assert!(!exts.iter().any(|e| e.name() == "Node"));
474474
}
475475

476476
#[test]
@@ -479,7 +479,7 @@ fn test_collect_extensions_node_with_version() {
479479
parse_markdown("---\nname: test\ndescription: test\nruntimes:\n node:\n version: '22.x'\n---\n")
480480
.unwrap();
481481
let exts = collect_extensions(&fm);
482-
assert!(exts.iter().any(|e| e.name() == "Node.js"));
482+
assert!(exts.iter().any(|e| e.name() == "Node"));
483483
}
484484

485485
#[test]
@@ -805,7 +805,7 @@ fn test_collect_extensions_all_runtimes_enabled() {
805805
let exts = collect_extensions(&fm);
806806
assert!(exts.iter().any(|e| e.name() == "Lean 4"));
807807
assert!(exts.iter().any(|e| e.name() == "Python"));
808-
assert!(exts.iter().any(|e| e.name() == "Node.js"));
808+
assert!(exts.iter().any(|e| e.name() == "Node"));
809809
assert!(exts.iter().any(|e| e.name() == "dotnet"));
810810
// All are Runtime phase
811811
let runtime_exts: Vec<_> = exts.iter().filter(|e| e.phase() == ExtensionPhase::Runtime).collect();

src/execute.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,16 @@ mod tests {
711711
assert!(result.is_err());
712712
}
713713

714+
#[tokio::test]
715+
async fn test_execute_malformed_upload_pipeline_artifact_returns_err() {
716+
// Missing required fields (artifact_name and file_path)
717+
let entry = serde_json::json!({"name": "upload-pipeline-artifact"});
718+
let ctx = ExecutionContext::default();
719+
720+
let result = execute_safe_output(&entry, &ctx).await;
721+
assert!(result.is_err());
722+
}
723+
714724
#[tokio::test]
715725
async fn test_execute_create_wiki_page_missing_context() {
716726
let entry = serde_json::json!({

src/runtimes/node/extension.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ impl NodeExtension {
2323

2424
impl CompilerExtension for NodeExtension {
2525
fn name(&self) -> &str {
26-
"Node.js"
26+
"Node"
2727
}
2828

2929
fn phase(&self) -> ExtensionPhase {

src/runtimes/node/mod.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,70 @@ pub fn generate_ensure_npmrc(config: &NodeRuntimeConfig) -> String {
166166
displayName: 'Ensure .npmrc exists'"
167167
)
168168
}
169+
170+
#[cfg(test)]
171+
mod tests {
172+
use super::*;
173+
174+
#[test]
175+
fn test_generate_node_install_default_version() {
176+
let config = NodeRuntimeConfig::Enabled(true);
177+
let step = generate_node_install(&config);
178+
assert!(
179+
step.contains("versionSpec: '22.x'"),
180+
"should default to 22.x, got: {step}"
181+
);
182+
assert!(step.contains("NodeTool@0"));
183+
assert!(step.contains("Install Node.js 22.x"));
184+
}
185+
186+
#[test]
187+
fn test_generate_node_install_pinned_version() {
188+
let config = NodeRuntimeConfig::WithOptions(NodeOptions {
189+
version: Some("20.x".into()),
190+
..Default::default()
191+
});
192+
let step = generate_node_install(&config);
193+
assert!(
194+
step.contains("versionSpec: '20.x'"),
195+
"should use pinned version, got: {step}"
196+
);
197+
assert!(step.contains("Install Node.js 20.x"));
198+
}
199+
200+
#[test]
201+
fn test_generate_npm_authenticate_emits_task() {
202+
let step = generate_npm_authenticate();
203+
assert!(step.contains("npmAuthenticate@0"));
204+
assert!(step.contains("workingFile: .npmrc"));
205+
}
206+
207+
#[test]
208+
fn test_generate_ensure_npmrc_default_registry() {
209+
let config = NodeRuntimeConfig::Enabled(true);
210+
let step = generate_ensure_npmrc(&config);
211+
assert!(
212+
step.contains("https://registry.npmjs.org/"),
213+
"should fallback to npm registry, got: {step}"
214+
);
215+
assert!(step.contains("Ensure .npmrc exists"));
216+
}
217+
218+
#[test]
219+
fn test_generate_ensure_npmrc_custom_feed_url() {
220+
let custom = "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/npm/registry/";
221+
let config = NodeRuntimeConfig::WithOptions(NodeOptions {
222+
feed_url: Some(custom.into()),
223+
..Default::default()
224+
});
225+
let step = generate_ensure_npmrc(&config);
226+
assert!(
227+
step.contains("pkgs.dev.azure.com"),
228+
"should use custom feed URL, got: {step}"
229+
);
230+
assert!(
231+
!step.contains("https://registry.npmjs.org/"),
232+
"should not fall back to default when custom feed is set, got: {step}"
233+
);
234+
}
235+
}

src/runtimes/python/mod.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,53 @@ pub fn generate_pip_authenticate() -> String {
136136
displayName: 'Authenticate pip (build service identity)'"
137137
.to_string()
138138
}
139+
140+
#[cfg(test)]
141+
mod tests {
142+
use super::*;
143+
144+
#[test]
145+
fn test_generate_python_install_default_version() {
146+
let config = PythonRuntimeConfig::Enabled(true);
147+
let step = generate_python_install(&config);
148+
assert!(
149+
step.contains("versionSpec: '3.x'"),
150+
"should default to 3.x, got: {step}"
151+
);
152+
assert!(
153+
step.contains("UsePythonVersion@0"),
154+
"should use UsePythonVersion task"
155+
);
156+
assert!(
157+
step.contains("Install Python 3.x"),
158+
"should set displayName"
159+
);
160+
}
161+
162+
#[test]
163+
fn test_generate_python_install_pinned_version() {
164+
let config = PythonRuntimeConfig::WithOptions(PythonOptions {
165+
version: Some("3.12".into()),
166+
..Default::default()
167+
});
168+
let step = generate_python_install(&config);
169+
assert!(
170+
step.contains("versionSpec: '3.12'"),
171+
"should use pinned version, got: {step}"
172+
);
173+
assert!(step.contains("Install Python 3.12"));
174+
}
175+
176+
#[test]
177+
fn test_generate_pip_authenticate_emits_task() {
178+
let step = generate_pip_authenticate();
179+
assert!(
180+
step.contains("PipAuthenticate@1"),
181+
"should emit PipAuthenticate task"
182+
);
183+
assert!(
184+
step.contains("artifactFeeds"),
185+
"should include artifactFeeds input"
186+
);
187+
}
188+
}

tests/compiler_tests.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2947,6 +2947,171 @@ runtimes:
29472947
let _ = fs::remove_dir_all(&temp_dir);
29482948
}
29492949

2950+
/// Integration test: `runtimes: python: true` end-to-end compilation
2951+
///
2952+
/// Verifies that a pipeline compiled with `runtimes: python: true` contains
2953+
/// the `UsePythonVersion@0` task and defaults to Python `3.x`.
2954+
#[test]
2955+
fn test_python_runtime_compiled_output() {
2956+
let temp_dir = std::env::temp_dir().join(format!(
2957+
"agentic-pipeline-python-{}",
2958+
std::process::id()
2959+
));
2960+
fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
2961+
2962+
let input = r#"---
2963+
name: "Python Agent"
2964+
description: "Agent with Python runtime"
2965+
runtimes:
2966+
python: true
2967+
safe-outputs:
2968+
noop: {}
2969+
---
2970+
2971+
## Python Agent
2972+
"#;
2973+
2974+
let input_path = temp_dir.join("python-agent.md");
2975+
let output_path = temp_dir.join("python-agent.yml");
2976+
fs::write(&input_path, input).expect("Failed to write test input");
2977+
2978+
let binary_path = PathBuf::from(env!("CARGO_BIN_EXE_ado-aw"));
2979+
let output = std::process::Command::new(&binary_path)
2980+
.args([
2981+
"compile",
2982+
input_path.to_str().unwrap(),
2983+
"-o",
2984+
output_path.to_str().unwrap(),
2985+
])
2986+
.output()
2987+
.expect("Failed to run compiler");
2988+
2989+
assert!(
2990+
output.status.success(),
2991+
"Compiler should succeed: {}",
2992+
String::from_utf8_lossy(&output.stderr)
2993+
);
2994+
2995+
let compiled = fs::read_to_string(&output_path).expect("Should read compiled YAML");
2996+
assert!(
2997+
compiled.contains("UsePythonVersion@0"),
2998+
"should have Python install step"
2999+
);
3000+
assert!(
3001+
compiled.contains("versionSpec: '3.x'"),
3002+
"should default to Python 3.x"
3003+
);
3004+
3005+
let _ = fs::remove_dir_all(&temp_dir);
3006+
}
3007+
3008+
/// Integration test: `runtimes: python:` with pinned version
3009+
#[test]
3010+
fn test_python_runtime_pinned_version_compiled_output() {
3011+
let temp_dir = std::env::temp_dir().join(format!(
3012+
"agentic-pipeline-python-pinned-{}",
3013+
std::process::id()
3014+
));
3015+
fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
3016+
3017+
let input = r#"---
3018+
name: "Python 3.12 Agent"
3019+
description: "Agent with pinned Python runtime"
3020+
runtimes:
3021+
python:
3022+
version: "3.12"
3023+
safe-outputs:
3024+
noop: {}
3025+
---
3026+
3027+
## Python Agent
3028+
"#;
3029+
3030+
let input_path = temp_dir.join("python-pinned-agent.md");
3031+
let output_path = temp_dir.join("python-pinned-agent.yml");
3032+
fs::write(&input_path, input).expect("Failed to write test input");
3033+
3034+
let binary_path = PathBuf::from(env!("CARGO_BIN_EXE_ado-aw"));
3035+
let output = std::process::Command::new(&binary_path)
3036+
.args([
3037+
"compile",
3038+
input_path.to_str().unwrap(),
3039+
"-o",
3040+
output_path.to_str().unwrap(),
3041+
])
3042+
.output()
3043+
.expect("Failed to run compiler");
3044+
3045+
assert!(
3046+
output.status.success(),
3047+
"Compiler should succeed: {}",
3048+
String::from_utf8_lossy(&output.stderr)
3049+
);
3050+
3051+
let compiled = fs::read_to_string(&output_path).expect("Should read compiled YAML");
3052+
assert!(
3053+
compiled.contains("versionSpec: '3.12'"),
3054+
"should use pinned version"
3055+
);
3056+
3057+
let _ = fs::remove_dir_all(&temp_dir);
3058+
}
3059+
3060+
/// Integration test: `runtimes: node: true` end-to-end compilation
3061+
#[test]
3062+
fn test_node_runtime_compiled_output() {
3063+
let temp_dir = std::env::temp_dir().join(format!(
3064+
"agentic-pipeline-node-{}",
3065+
std::process::id()
3066+
));
3067+
fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
3068+
3069+
let input = r#"---
3070+
name: "Node Agent"
3071+
description: "Agent with Node runtime"
3072+
runtimes:
3073+
node: true
3074+
safe-outputs:
3075+
noop: {}
3076+
---
3077+
3078+
## Node Agent
3079+
"#;
3080+
3081+
let input_path = temp_dir.join("node-agent.md");
3082+
let output_path = temp_dir.join("node-agent.yml");
3083+
fs::write(&input_path, input).expect("Failed to write test input");
3084+
3085+
let binary_path = PathBuf::from(env!("CARGO_BIN_EXE_ado-aw"));
3086+
let output = std::process::Command::new(&binary_path)
3087+
.args([
3088+
"compile",
3089+
input_path.to_str().unwrap(),
3090+
"-o",
3091+
output_path.to_str().unwrap(),
3092+
])
3093+
.output()
3094+
.expect("Failed to run compiler");
3095+
3096+
assert!(
3097+
output.status.success(),
3098+
"Compiler should succeed: {}",
3099+
String::from_utf8_lossy(&output.stderr)
3100+
);
3101+
3102+
let compiled = fs::read_to_string(&output_path).expect("Should read compiled YAML");
3103+
assert!(
3104+
compiled.contains("NodeTool@0"),
3105+
"should have Node install step"
3106+
);
3107+
assert!(
3108+
compiled.contains("versionSpec: '22.x'"),
3109+
"should default to Node 22.x"
3110+
);
3111+
3112+
let _ = fs::remove_dir_all(&temp_dir);
3113+
}
3114+
29503115
/// Integration test: `schedule:` object form with `branches:` end-to-end compilation
29513116
///
29523117
/// Verifies that a pipeline compiled with the object-form schedule containing

tests/mcp_http_tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@ fn test_mcp_initialize_and_tools_list() {
301301
body.contains("missing-data"),
302302
"Should list missing-data tool, body: {body}"
303303
);
304+
assert!(
305+
body.contains("upload-pipeline-artifact"),
306+
"Should list upload-pipeline-artifact tool, body: {body}"
307+
);
304308
}
305309

306310
#[test]

0 commit comments

Comments
 (0)