From 0f6649db15cda35cb83b1b2d5432e2ff49c04ecb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 12:08:26 +0000 Subject: [PATCH 1/2] Initial plan From c5f55d252915e45c91982955d231892cdd903512 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 12:14:48 +0000 Subject: [PATCH 2/2] 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> --- src/compile/extensions/tests.rs | 8 +- src/execute.rs | 10 ++ src/runtimes/node/extension.rs | 2 +- src/runtimes/node/mod.rs | 67 +++++++++++++ src/runtimes/python/mod.rs | 50 ++++++++++ tests/compiler_tests.rs | 165 ++++++++++++++++++++++++++++++++ tests/mcp_http_tests.rs | 4 + 7 files changed, 301 insertions(+), 5 deletions(-) diff --git a/src/compile/extensions/tests.rs b/src/compile/extensions/tests.rs index a0901468..fdc86a55 100644 --- a/src/compile/extensions/tests.rs +++ b/src/compile/extensions/tests.rs @@ -461,7 +461,7 @@ fn test_collect_extensions_node_enabled() { parse_markdown("---\nname: test\ndescription: test\nruntimes:\n node: true\n---\n") .unwrap(); let exts = collect_extensions(&fm); - assert!(exts.iter().any(|e| e.name() == "Node.js")); + assert!(exts.iter().any(|e| e.name() == "Node")); } #[test] @@ -470,7 +470,7 @@ fn test_collect_extensions_node_disabled() { parse_markdown("---\nname: test\ndescription: test\nruntimes:\n node: false\n---\n") .unwrap(); let exts = collect_extensions(&fm); - assert!(!exts.iter().any(|e| e.name() == "Node.js")); + assert!(!exts.iter().any(|e| e.name() == "Node")); } #[test] @@ -479,7 +479,7 @@ fn test_collect_extensions_node_with_version() { parse_markdown("---\nname: test\ndescription: test\nruntimes:\n node:\n version: '22.x'\n---\n") .unwrap(); let exts = collect_extensions(&fm); - assert!(exts.iter().any(|e| e.name() == "Node.js")); + assert!(exts.iter().any(|e| e.name() == "Node")); } #[test] @@ -805,7 +805,7 @@ fn test_collect_extensions_all_runtimes_enabled() { let exts = collect_extensions(&fm); assert!(exts.iter().any(|e| e.name() == "Lean 4")); assert!(exts.iter().any(|e| e.name() == "Python")); - assert!(exts.iter().any(|e| e.name() == "Node.js")); + assert!(exts.iter().any(|e| e.name() == "Node")); assert!(exts.iter().any(|e| e.name() == "dotnet")); // All are Runtime phase let runtime_exts: Vec<_> = exts.iter().filter(|e| e.phase() == ExtensionPhase::Runtime).collect(); diff --git a/src/execute.rs b/src/execute.rs index 583c3afa..6ec8ebe7 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -711,6 +711,16 @@ mod tests { assert!(result.is_err()); } + #[tokio::test] + async fn test_execute_malformed_upload_pipeline_artifact_returns_err() { + // Missing required fields (artifact_name and file_path) + let entry = serde_json::json!({"name": "upload-pipeline-artifact"}); + let ctx = ExecutionContext::default(); + + let result = execute_safe_output(&entry, &ctx).await; + assert!(result.is_err()); + } + #[tokio::test] async fn test_execute_create_wiki_page_missing_context() { let entry = serde_json::json!({ diff --git a/src/runtimes/node/extension.rs b/src/runtimes/node/extension.rs index c57ca4c2..7faaa9a7 100644 --- a/src/runtimes/node/extension.rs +++ b/src/runtimes/node/extension.rs @@ -23,7 +23,7 @@ impl NodeExtension { impl CompilerExtension for NodeExtension { fn name(&self) -> &str { - "Node.js" + "Node" } fn phase(&self) -> ExtensionPhase { diff --git a/src/runtimes/node/mod.rs b/src/runtimes/node/mod.rs index 4d235c69..8933b81d 100644 --- a/src/runtimes/node/mod.rs +++ b/src/runtimes/node/mod.rs @@ -166,3 +166,70 @@ pub fn generate_ensure_npmrc(config: &NodeRuntimeConfig) -> String { displayName: 'Ensure .npmrc exists'" ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_node_install_default_version() { + let config = NodeRuntimeConfig::Enabled(true); + let step = generate_node_install(&config); + assert!( + step.contains("versionSpec: '22.x'"), + "should default to 22.x, got: {step}" + ); + assert!(step.contains("NodeTool@0")); + assert!(step.contains("Install Node.js 22.x")); + } + + #[test] + fn test_generate_node_install_pinned_version() { + let config = NodeRuntimeConfig::WithOptions(NodeOptions { + version: Some("20.x".into()), + ..Default::default() + }); + let step = generate_node_install(&config); + assert!( + step.contains("versionSpec: '20.x'"), + "should use pinned version, got: {step}" + ); + assert!(step.contains("Install Node.js 20.x")); + } + + #[test] + fn test_generate_npm_authenticate_emits_task() { + let step = generate_npm_authenticate(); + assert!(step.contains("npmAuthenticate@0")); + assert!(step.contains("workingFile: .npmrc")); + } + + #[test] + fn test_generate_ensure_npmrc_default_registry() { + let config = NodeRuntimeConfig::Enabled(true); + let step = generate_ensure_npmrc(&config); + assert!( + step.contains("https://registry.npmjs.org/"), + "should fallback to npm registry, got: {step}" + ); + assert!(step.contains("Ensure .npmrc exists")); + } + + #[test] + fn test_generate_ensure_npmrc_custom_feed_url() { + let custom = "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/npm/registry/"; + let config = NodeRuntimeConfig::WithOptions(NodeOptions { + feed_url: Some(custom.into()), + ..Default::default() + }); + let step = generate_ensure_npmrc(&config); + assert!( + step.contains("pkgs.dev.azure.com"), + "should use custom feed URL, got: {step}" + ); + assert!( + !step.contains("https://registry.npmjs.org/"), + "should not fall back to default when custom feed is set, got: {step}" + ); + } +} diff --git a/src/runtimes/python/mod.rs b/src/runtimes/python/mod.rs index 70ab88da..af2ba632 100644 --- a/src/runtimes/python/mod.rs +++ b/src/runtimes/python/mod.rs @@ -136,3 +136,53 @@ pub fn generate_pip_authenticate() -> String { displayName: 'Authenticate pip (build service identity)'" .to_string() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_python_install_default_version() { + let config = PythonRuntimeConfig::Enabled(true); + let step = generate_python_install(&config); + assert!( + step.contains("versionSpec: '3.x'"), + "should default to 3.x, got: {step}" + ); + assert!( + step.contains("UsePythonVersion@0"), + "should use UsePythonVersion task" + ); + assert!( + step.contains("Install Python 3.x"), + "should set displayName" + ); + } + + #[test] + fn test_generate_python_install_pinned_version() { + let config = PythonRuntimeConfig::WithOptions(PythonOptions { + version: Some("3.12".into()), + ..Default::default() + }); + let step = generate_python_install(&config); + assert!( + step.contains("versionSpec: '3.12'"), + "should use pinned version, got: {step}" + ); + assert!(step.contains("Install Python 3.12")); + } + + #[test] + fn test_generate_pip_authenticate_emits_task() { + let step = generate_pip_authenticate(); + assert!( + step.contains("PipAuthenticate@1"), + "should emit PipAuthenticate task" + ); + assert!( + step.contains("artifactFeeds"), + "should include artifactFeeds input" + ); + } +} diff --git a/tests/compiler_tests.rs b/tests/compiler_tests.rs index 05ea4fad..abb79548 100644 --- a/tests/compiler_tests.rs +++ b/tests/compiler_tests.rs @@ -2794,6 +2794,171 @@ Prove theorems and build Lean 4 projects. let _ = fs::remove_dir_all(&temp_dir); } +/// Integration test: `runtimes: python: true` end-to-end compilation +/// +/// Verifies that a pipeline compiled with `runtimes: python: true` contains +/// the `UsePythonVersion@0` task and defaults to Python `3.x`. +#[test] +fn test_python_runtime_compiled_output() { + let temp_dir = std::env::temp_dir().join(format!( + "agentic-pipeline-python-{}", + std::process::id() + )); + fs::create_dir_all(&temp_dir).expect("Failed to create temp directory"); + + let input = r#"--- +name: "Python Agent" +description: "Agent with Python runtime" +runtimes: + python: true +safe-outputs: + noop: {} +--- + +## Python Agent +"#; + + let input_path = temp_dir.join("python-agent.md"); + let output_path = temp_dir.join("python-agent.yml"); + fs::write(&input_path, input).expect("Failed to write test input"); + + let binary_path = PathBuf::from(env!("CARGO_BIN_EXE_ado-aw")); + let output = std::process::Command::new(&binary_path) + .args([ + "compile", + input_path.to_str().unwrap(), + "-o", + output_path.to_str().unwrap(), + ]) + .output() + .expect("Failed to run compiler"); + + assert!( + output.status.success(), + "Compiler should succeed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let compiled = fs::read_to_string(&output_path).expect("Should read compiled YAML"); + assert!( + compiled.contains("UsePythonVersion@0"), + "should have Python install step" + ); + assert!( + compiled.contains("versionSpec: '3.x'"), + "should default to Python 3.x" + ); + + let _ = fs::remove_dir_all(&temp_dir); +} + +/// Integration test: `runtimes: python:` with pinned version +#[test] +fn test_python_runtime_pinned_version_compiled_output() { + let temp_dir = std::env::temp_dir().join(format!( + "agentic-pipeline-python-pinned-{}", + std::process::id() + )); + fs::create_dir_all(&temp_dir).expect("Failed to create temp directory"); + + let input = r#"--- +name: "Python 3.12 Agent" +description: "Agent with pinned Python runtime" +runtimes: + python: + version: "3.12" +safe-outputs: + noop: {} +--- + +## Python Agent +"#; + + let input_path = temp_dir.join("python-pinned-agent.md"); + let output_path = temp_dir.join("python-pinned-agent.yml"); + fs::write(&input_path, input).expect("Failed to write test input"); + + let binary_path = PathBuf::from(env!("CARGO_BIN_EXE_ado-aw")); + let output = std::process::Command::new(&binary_path) + .args([ + "compile", + input_path.to_str().unwrap(), + "-o", + output_path.to_str().unwrap(), + ]) + .output() + .expect("Failed to run compiler"); + + assert!( + output.status.success(), + "Compiler should succeed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let compiled = fs::read_to_string(&output_path).expect("Should read compiled YAML"); + assert!( + compiled.contains("versionSpec: '3.12'"), + "should use pinned version" + ); + + let _ = fs::remove_dir_all(&temp_dir); +} + +/// Integration test: `runtimes: node: true` end-to-end compilation +#[test] +fn test_node_runtime_compiled_output() { + let temp_dir = std::env::temp_dir().join(format!( + "agentic-pipeline-node-{}", + std::process::id() + )); + fs::create_dir_all(&temp_dir).expect("Failed to create temp directory"); + + let input = r#"--- +name: "Node Agent" +description: "Agent with Node runtime" +runtimes: + node: true +safe-outputs: + noop: {} +--- + +## Node Agent +"#; + + let input_path = temp_dir.join("node-agent.md"); + let output_path = temp_dir.join("node-agent.yml"); + fs::write(&input_path, input).expect("Failed to write test input"); + + let binary_path = PathBuf::from(env!("CARGO_BIN_EXE_ado-aw")); + let output = std::process::Command::new(&binary_path) + .args([ + "compile", + input_path.to_str().unwrap(), + "-o", + output_path.to_str().unwrap(), + ]) + .output() + .expect("Failed to run compiler"); + + assert!( + output.status.success(), + "Compiler should succeed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let compiled = fs::read_to_string(&output_path).expect("Should read compiled YAML"); + assert!( + compiled.contains("NodeTool@0"), + "should have Node install step" + ); + assert!( + compiled.contains("versionSpec: '22.x'"), + "should default to Node 22.x" + ); + + let _ = fs::remove_dir_all(&temp_dir); +} + /// Integration test: `schedule:` object form with `branches:` end-to-end compilation /// /// Verifies that a pipeline compiled with the object-form schedule containing diff --git a/tests/mcp_http_tests.rs b/tests/mcp_http_tests.rs index 9b6b7811..5f0d8906 100644 --- a/tests/mcp_http_tests.rs +++ b/tests/mcp_http_tests.rs @@ -301,6 +301,10 @@ fn test_mcp_initialize_and_tools_list() { body.contains("missing-data"), "Should list missing-data tool, body: {body}" ); + assert!( + body.contains("upload-pipeline-artifact"), + "Should list upload-pipeline-artifact tool, body: {body}" + ); } #[test]