From f1b288f5af6d6f2034382892c3f528092b07d6ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:19:00 +0000 Subject: [PATCH 1/3] Initial plan From c27a112377ddde6ccdda2e647a0b85bcf24d55e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:27:35 +0000 Subject: [PATCH 2/3] test: fill 5 test gaps in result handling, config sanitization and integration Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/38a827f0-9cc7-4b71-af40-8542ed3014ef Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --- src/safeoutputs/result.rs | 57 ++++++++++++ src/sanitize.rs | 16 ++++ tests/compiler_tests.rs | 178 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+) diff --git a/src/safeoutputs/result.rs b/src/safeoutputs/result.rs index 4678f75..ce32c4a 100644 --- a/src/safeoutputs/result.rs +++ b/src/safeoutputs/result.rs @@ -511,4 +511,61 @@ 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 that can be used with get_tool_config. + #[derive(Default, serde::Deserialize)] + struct TestToolConfig { + value: String, + } + + impl crate::sanitize::SanitizeConfig for TestToolConfig { + 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: TestToolConfig = 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 + ); + } } diff --git a/src/sanitize.rs b/src/sanitize.rs index 1fa52c3..61b3857 100644 --- a/src/sanitize.rs +++ b/src/sanitize.rs @@ -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!"; diff --git a/tests/compiler_tests.rs b/tests/compiler_tests.rs index db499af..7577020 100644 --- a/tests/compiler_tests.rs +++ b/tests/compiler_tests.rs @@ -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() { From 6b6d8f4f66270c8b7a1a4dda14361ef2a675b862 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 15:32:08 +0000 Subject: [PATCH 3/3] test: rename TestToolConfig to TestConfigForSanitization for clarity Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/38a827f0-9cc7-4b71-af40-8542ed3014ef Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --- src/safeoutputs/result.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/safeoutputs/result.rs b/src/safeoutputs/result.rs index ce32c4a..a1d1175 100644 --- a/src/safeoutputs/result.rs +++ b/src/safeoutputs/result.rs @@ -537,13 +537,14 @@ mod tests { // ── ExecutionContext::get_tool_config sanitization tests ────────────── - /// Test config struct that can be used with get_tool_config. + /// Test config struct used to verify that `get_tool_config` applies + /// `sanitize_config_fields()` before returning the deserialized value. #[derive(Default, serde::Deserialize)] - struct TestToolConfig { + struct TestConfigForSanitization { value: String, } - impl crate::sanitize::SanitizeConfig for TestToolConfig { + impl crate::sanitize::SanitizeConfig for TestConfigForSanitization { fn sanitize_config_fields(&mut self) { self.value = crate::sanitize::sanitize_config(&self.value); } @@ -556,7 +557,7 @@ mod tests { "my-tool".to_string(), serde_json::json!({ "value": "##vso[task.setvariable variable=secret]injected" }), ); - let config: TestToolConfig = ctx.get_tool_config("my-tool"); + let config: TestConfigForSanitization = ctx.get_tool_config("my-tool"); assert!( !config.value.contains("##vso[task."), "Injected ##vso[ command should be neutralized; got: {}",