Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/compile/extensions/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand Down Expand Up @@ -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();
Expand Down
10 changes: 10 additions & 0 deletions src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!({
Expand Down
2 changes: 1 addition & 1 deletion src/runtimes/node/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl NodeExtension {

impl CompilerExtension for NodeExtension {
fn name(&self) -> &str {
"Node.js"
"Node"
}

fn phase(&self) -> ExtensionPhase {
Expand Down
67 changes: 67 additions & 0 deletions src/runtimes/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
);
}
}
50 changes: 50 additions & 0 deletions src/runtimes/python/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}
}
165 changes: 165 additions & 0 deletions tests/compiler_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2947,6 +2947,171 @@ runtimes:
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
Expand Down
4 changes: 4 additions & 0 deletions tests/mcp_http_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down