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
58 changes: 58 additions & 0 deletions src/safeoutputs/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,4 +511,62 @@ mod tests {
let mcp_err = anyhow_to_mcp_error(err);
assert_eq!(mcp_err.code, rmcp::model::ErrorCode::INVALID_PARAMS);
}

// ── ExecutionResult::warning / is_warning tests ───────────────────────

#[test]
fn test_execution_result_warning_sets_success_and_warning() {
let r = ExecutionResult::warning("PR created but auto-complete failed");
assert!(r.success, "warning result should have success=true");
assert!(r.is_warning(), "warning result should have warning=true");
assert_eq!(r.message, "PR created but auto-complete failed");
assert!(r.data.is_none());
}

#[test]
fn test_execution_result_success_is_not_warning() {
let r = ExecutionResult::success("all good");
assert!(!r.is_warning(), "success result should not be a warning");
}

#[test]
fn test_execution_result_failure_is_not_warning() {
let r = ExecutionResult::failure("something broke");
assert!(!r.is_warning(), "failure result should not be a warning");
}

// ── ExecutionContext::get_tool_config sanitization tests ──────────────

/// Test config struct used to verify that `get_tool_config` applies
/// `sanitize_config_fields()` before returning the deserialized value.
#[derive(Default, serde::Deserialize)]
struct TestConfigForSanitization {
value: String,
}

impl crate::sanitize::SanitizeConfig for TestConfigForSanitization {
fn sanitize_config_fields(&mut self) {
self.value = crate::sanitize::sanitize_config(&self.value);
}
}

#[test]
fn test_get_tool_config_sanitizes_vso_pipeline_command() {
let mut ctx = ExecutionContext::default();
ctx.tool_configs.insert(
"my-tool".to_string(),
serde_json::json!({ "value": "##vso[task.setvariable variable=secret]injected" }),
);
let config: TestConfigForSanitization = ctx.get_tool_config("my-tool");
assert!(
!config.value.contains("##vso[task."),
"Injected ##vso[ command should be neutralized; got: {}",
config.value
);
assert!(
config.value.contains("`##vso[`"),
"Pipeline command should be wrapped in backticks; got: {}",
config.value
);
}
}
16 changes: 16 additions & 0 deletions src/sanitize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,22 @@ mod tests {
assert!(!result.contains("##vso[task."));
}

#[test]
fn test_sanitize_config_neutralizes_shorthand_pipeline_command() {
let input = "##[error]bad";
let result = sanitize_config(input);
assert!(
result.contains("`##[`"),
"##[ shorthand should be wrapped in backticks; got: {}",
result
);
assert!(
!result.contains("##[error]"),
"##[error] should be neutralized; got: {}",
result
);
}

#[test]
fn test_sanitize_config_removes_control_chars() {
let input = "hello\x00world\x07!";
Expand Down
178 changes: 178 additions & 0 deletions tests/compiler_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2695,6 +2695,184 @@ network:
let _ = fs::remove_dir_all(&temp_dir);
}

/// Integration test: `runtimes: lean: true` end-to-end compilation
///
/// Verifies that a pipeline compiled with `runtimes: lean: true` contains:
/// - The elan installer step (`elan-init.sh`)
/// - Lean ecosystem domains in the network allow-list (`elan.lean-lang.org`)
/// - Lean tool shell allow-args (`shell(lean)`, `shell(lake)`, `shell(elan)`)
/// - No unreplaced `{{ }}` template markers
#[test]
fn test_lean_runtime_compiled_output() {
let temp_dir = std::env::temp_dir().join(format!(
"agentic-pipeline-lean-{}",
std::process::id()
));
fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");

let input = r#"---
name: "Lean Agent"
description: "Agent with Lean 4 runtime"
runtimes:
lean: true
---

## Lean Agent

Prove theorems and build Lean 4 projects.
"#;

let input_path = temp_dir.join("lean-agent.md");
let output_path = temp_dir.join("lean-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)
);
assert!(output_path.exists(), "Compiled YAML should exist");

let compiled = fs::read_to_string(&output_path).expect("Should read compiled YAML");

// Lean runtime installs elan via the elan-init.sh script
assert!(
compiled.contains("elan-init.sh"),
"Compiled output should include elan-init.sh installer step"
);

// Lean ecosystem domains should appear in the AWF allow-domains list
assert!(
compiled.contains("elan.lean-lang.org"),
"Compiled output should include elan.lean-lang.org in allowed domains"
);

// Lean tools should appear as shell allow-args for the Copilot CLI
assert!(
compiled.contains("shell(lean)"),
"Compiled output should include shell(lean) in --allow-tool args"
);
assert!(
compiled.contains("shell(lake)"),
"Compiled output should include shell(lake) in --allow-tool args"
);
assert!(
compiled.contains("shell(elan)"),
"Compiled output should include shell(elan) in --allow-tool args"
);

// Verify no unreplaced {{ markers }} remain
for line in compiled.lines() {
let stripped = line.replace("${{", "");
assert!(
!stripped.contains("{{ "),
"Compiled output should not contain unreplaced marker: {}",
line.trim()
);
}

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
/// explicit branch filters generates a `branches.include` block in the output.
#[test]
fn test_schedule_object_form_with_branches_compiled_output() {
let temp_dir = std::env::temp_dir().join(format!(
"agentic-pipeline-schedule-branches-{}",
std::process::id()
));
fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");

let input = r#"---
name: "Scheduled Agent"
description: "Agent with branch-filtered schedule"
schedule:
run: daily around 14:00
branches:
- main
- release/*
---

## Scheduled Agent

Run daily on specific branches.
"#;

let input_path = temp_dir.join("scheduled-agent.md");
let output_path = temp_dir.join("scheduled-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)
);
assert!(output_path.exists(), "Compiled YAML should exist");

let compiled = fs::read_to_string(&output_path).expect("Should read compiled YAML");

// Should contain a schedules block
assert!(
compiled.contains("schedules:"),
"Compiled output should contain a schedules block"
);

// Should contain the branches.include block with both branches
assert!(
compiled.contains("branches:"),
"Compiled output should contain a branches filter"
);
assert!(
compiled.contains("include:"),
"Compiled output should contain an include list under branches"
);
assert!(
compiled.contains("- main"),
"Compiled output should include 'main' branch"
);
assert!(
compiled.contains("- release/*"),
"Compiled output should include 'release/*' branch"
);

// Verify no unreplaced {{ markers }} remain
for line in compiled.lines() {
let stripped = line.replace("${{", "");
assert!(
!stripped.contains("{{ "),
"Compiled output should not contain unreplaced marker: {}",
line.trim()
);
}

let _ = fs::remove_dir_all(&temp_dir);
}

/// Test that network.allowed with a bare '*' fails compilation
#[test]
fn test_network_allow_bare_wildcard_fails() {
Expand Down
Loading