Skip to content

Commit d06a059

Browse files
[WIP] Fix test gaps in compile/common.rs and compile/onees.rs (#163)
* Initial plan * test: add missing unit tests for compile/common.rs, compile/onees.rs, and fuzzy_schedule.rs Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/cc9ca027-17fc-4675-abd6-a6e5ff2e8b2c 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 a46a85b commit d06a059

3 files changed

Lines changed: 340 additions & 0 deletions

File tree

src/compile/common.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,4 +1786,155 @@ mod tests {
17861786
let args = generate_enabled_tools_args(&fm);
17871787
assert!(args.is_empty(), "memory-only safe-outputs should produce no args (all tools available)");
17881788
}
1789+
1790+
// ─── replace_with_indent ─────────────────────────────────────────────────
1791+
1792+
#[test]
1793+
fn test_replace_with_indent_multiline_replacement() {
1794+
let template = "steps:\n {{ my_marker }}\n";
1795+
let replacement = "- bash: echo hello\n displayName: Hello";
1796+
let result = replace_with_indent(template, "{{ my_marker }}", replacement);
1797+
// The 4-space indent on the placeholder line is inherited by continuation lines
1798+
assert_eq!(result, "steps:\n - bash: echo hello\n displayName: Hello\n");
1799+
}
1800+
1801+
#[test]
1802+
fn test_replace_with_indent_not_at_line_start_no_indent() {
1803+
// When the placeholder is not at the start of a line (preceded by non-whitespace),
1804+
// no extra indentation is added to continuation lines.
1805+
let template = "prefix {{ marker }} suffix";
1806+
let result = replace_with_indent(template, "{{ marker }}", "VALUE");
1807+
assert_eq!(result, "prefix VALUE suffix");
1808+
}
1809+
1810+
#[test]
1811+
fn test_replace_with_indent_single_line_replacement_preserves_trailing_newline() {
1812+
let template = " {{ placeholder }}\n";
1813+
let result = replace_with_indent(template, "{{ placeholder }}", "value");
1814+
assert_eq!(result, " value\n");
1815+
}
1816+
1817+
#[test]
1818+
fn test_replace_with_indent_replacement_ending_with_newline() {
1819+
let template = " {{ placeholder }}\n";
1820+
let result = replace_with_indent(template, "{{ placeholder }}", "line1\nline2\n");
1821+
// The trailing \n in the replacement should be preserved
1822+
assert!(result.contains("line1"));
1823+
assert!(result.contains("line2"));
1824+
assert!(result.ends_with('\n'));
1825+
}
1826+
1827+
// ─── format_step_yaml / format_step_yaml_indented ────────────────────────
1828+
1829+
#[test]
1830+
fn test_format_step_yaml_single_line() {
1831+
let result = format_step_yaml("bash: echo hi");
1832+
assert_eq!(result, " - bash: echo hi");
1833+
}
1834+
1835+
#[test]
1836+
fn test_format_step_yaml_multiline() {
1837+
let result = format_step_yaml("bash: |\n echo hi\n echo bye");
1838+
let lines: Vec<&str> = result.lines().collect();
1839+
assert_eq!(lines[0], " - bash: |");
1840+
// Continuation lines get 8 spaces prepended (existing indent is preserved)
1841+
assert_eq!(lines[1], " echo hi");
1842+
assert_eq!(lines[2], " echo bye");
1843+
}
1844+
1845+
#[test]
1846+
fn test_format_step_yaml_strips_yaml_document_separator() {
1847+
let result = format_step_yaml("--- bash: echo hi");
1848+
assert_eq!(result, " - bash: echo hi");
1849+
}
1850+
1851+
#[test]
1852+
fn test_format_step_yaml_indented_custom_base() {
1853+
let result = format_step_yaml_indented("bash: echo hi", 6);
1854+
assert_eq!(result, " - bash: echo hi");
1855+
}
1856+
1857+
#[test]
1858+
fn test_format_step_yaml_indented_zero_base() {
1859+
let result = format_step_yaml_indented("bash: echo hi", 0);
1860+
assert_eq!(result, "- bash: echo hi");
1861+
}
1862+
1863+
// ─── generate_acquire_ado_token ──────────────────────────────────────────
1864+
1865+
#[test]
1866+
fn test_generate_acquire_ado_token_with_sc() {
1867+
let result = generate_acquire_ado_token(Some("my-arm-sc"), "SC_READ_TOKEN");
1868+
assert!(result.contains("AzureCLI@2"), "Should use AzureCLI@2 task");
1869+
assert!(
1870+
result.contains("azureSubscription: 'my-arm-sc'"),
1871+
"Should embed service connection name"
1872+
);
1873+
assert!(
1874+
result.contains("variable=SC_READ_TOKEN;issecret=true"),
1875+
"Should set correct pipeline variable as secret"
1876+
);
1877+
assert!(
1878+
result.contains("az account get-access-token"),
1879+
"Should call az CLI to get access token"
1880+
);
1881+
}
1882+
1883+
#[test]
1884+
fn test_generate_acquire_ado_token_none_returns_empty() {
1885+
let result = generate_acquire_ado_token(None, "SC_READ_TOKEN");
1886+
assert!(result.is_empty(), "None service connection should return empty string");
1887+
}
1888+
1889+
#[test]
1890+
fn test_generate_acquire_ado_token_write_token_variable() {
1891+
let result = generate_acquire_ado_token(Some("write-sc"), "SC_WRITE_TOKEN");
1892+
assert!(result.contains("variable=SC_WRITE_TOKEN;issecret=true"));
1893+
assert!(!result.contains("SC_READ_TOKEN"));
1894+
}
1895+
1896+
// ─── generate_copilot_ado_env / generate_executor_ado_env ────────────────
1897+
1898+
#[test]
1899+
fn test_generate_copilot_ado_env_with_connection() {
1900+
let result = generate_copilot_ado_env(Some("my-sc"));
1901+
assert!(
1902+
result.contains("AZURE_DEVOPS_EXT_PAT: $(SC_READ_TOKEN)"),
1903+
"Should set AZURE_DEVOPS_EXT_PAT to SC_READ_TOKEN"
1904+
);
1905+
assert!(
1906+
result.contains("SYSTEM_ACCESSTOKEN: $(SC_READ_TOKEN)"),
1907+
"Should set SYSTEM_ACCESSTOKEN to SC_READ_TOKEN"
1908+
);
1909+
}
1910+
1911+
#[test]
1912+
fn test_generate_copilot_ado_env_none_empty() {
1913+
assert!(
1914+
generate_copilot_ado_env(None).is_empty(),
1915+
"None service connection should produce empty env block"
1916+
);
1917+
}
1918+
1919+
#[test]
1920+
fn test_generate_executor_ado_env_with_connection() {
1921+
let result = generate_executor_ado_env(Some("my-sc"));
1922+
assert!(
1923+
result.contains("SYSTEM_ACCESSTOKEN: $(SC_WRITE_TOKEN)"),
1924+
"Executor should use SC_WRITE_TOKEN"
1925+
);
1926+
// Must NOT expose the read token in the executor env
1927+
assert!(
1928+
!result.contains("SC_READ_TOKEN"),
1929+
"Executor env must not contain SC_READ_TOKEN"
1930+
);
1931+
}
1932+
1933+
#[test]
1934+
fn test_generate_executor_ado_env_none_empty() {
1935+
assert!(
1936+
generate_executor_ado_env(None).is_empty(),
1937+
"None service connection should produce empty env block"
1938+
);
1939+
}
17891940
}

src/compile/onees.rs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,160 @@ fn generate_teardown_job(teardown_steps: &[serde_yaml::Value], agent_name: &str)
388388
steps_yaml.join("\n ")
389389
)
390390
}
391+
392+
#[cfg(test)]
393+
mod tests {
394+
use super::*;
395+
use super::super::types::McpOptions;
396+
397+
// ─── generate_agent_context_root ─────────────────────────────────────────
398+
399+
#[test]
400+
fn test_generate_agent_context_root_repo() {
401+
assert_eq!(
402+
generate_agent_context_root("repo"),
403+
"$(Build.Repository.Name)"
404+
);
405+
}
406+
407+
#[test]
408+
fn test_generate_agent_context_root_root() {
409+
assert_eq!(generate_agent_context_root("root"), ".");
410+
}
411+
412+
#[test]
413+
fn test_generate_agent_context_root_unknown_defaults_to_dot() {
414+
// Any unrecognised workspace value should fall through to "."
415+
assert_eq!(generate_agent_context_root("something-else"), ".");
416+
}
417+
418+
// ─── generate_mcp_configuration ──────────────────────────────────────────
419+
420+
#[test]
421+
fn test_generate_mcp_configuration_empty_returns_braces() {
422+
let mcps = HashMap::new();
423+
let result = generate_mcp_configuration(&mcps);
424+
assert_eq!(result, "{}");
425+
}
426+
427+
#[test]
428+
fn test_generate_mcp_configuration_skips_custom_mcp_with_command() {
429+
let mut mcps = HashMap::new();
430+
mcps.insert(
431+
"my-tool".to_string(),
432+
McpConfig::WithOptions(McpOptions {
433+
command: Some("node".to_string()),
434+
..Default::default()
435+
}),
436+
);
437+
let result = generate_mcp_configuration(&mcps);
438+
// Custom MCPs with `command:` are not supported in 1ES — must be excluded
439+
assert!(
440+
!result.contains("my-tool"),
441+
"Custom MCP with command should be excluded in 1ES target"
442+
);
443+
assert_eq!(result, "{}", "Only custom MCPs → empty config");
444+
}
445+
446+
#[test]
447+
fn test_generate_mcp_configuration_service_connection_mcp() {
448+
let mut mcps = HashMap::new();
449+
mcps.insert(
450+
"my-mcp".to_string(),
451+
McpConfig::WithOptions(McpOptions {
452+
service_connection: Some("mcp-my-mcp-sc".to_string()),
453+
..Default::default()
454+
}),
455+
);
456+
let result = generate_mcp_configuration(&mcps);
457+
assert!(result.contains("my-mcp"), "Service-connection MCP should appear in output");
458+
assert!(
459+
result.contains("serviceConnection: mcp-my-mcp-sc"),
460+
"Should reference the explicit service connection"
461+
);
462+
}
463+
464+
#[test]
465+
fn test_generate_mcp_configuration_default_service_connection_naming() {
466+
// When no explicit service_connection is set, a default name is generated.
467+
let mut mcps = HashMap::new();
468+
mcps.insert("my-tool".to_string(), McpConfig::Enabled(true));
469+
let result = generate_mcp_configuration(&mcps);
470+
assert!(result.contains("my-tool"));
471+
assert!(result.contains("serviceConnection: mcp-my-tool-service-connection"));
472+
}
473+
474+
#[test]
475+
fn test_generate_mcp_configuration_disabled_mcp_excluded() {
476+
let mut mcps = HashMap::new();
477+
mcps.insert("disabled-mcp".to_string(), McpConfig::Enabled(false));
478+
let result = generate_mcp_configuration(&mcps);
479+
assert!(!result.contains("disabled-mcp"), "Disabled MCP should not appear in output");
480+
assert_eq!(result, "{}");
481+
}
482+
483+
// ─── generate_inline_steps ────────────────────────────────────────────────
484+
485+
#[test]
486+
fn test_generate_inline_steps_empty() {
487+
let result = generate_inline_steps(&[]);
488+
assert!(result.is_empty(), "Empty steps list should return empty string");
489+
}
490+
491+
#[test]
492+
fn test_generate_inline_steps_single_step() {
493+
let step: serde_yaml::Value =
494+
serde_yaml::from_str("bash: echo hello").expect("valid yaml");
495+
let result = generate_inline_steps(&[step]);
496+
assert!(result.contains("bash"), "Step YAML should contain the bash key");
497+
assert!(result.contains("echo hello"), "Step YAML should contain the command");
498+
}
499+
500+
// ─── generate_setup_job ──────────────────────────────────────────────────
501+
502+
#[test]
503+
fn test_generate_setup_job_empty_steps() {
504+
let result = generate_setup_job(&[], "My Agent");
505+
assert!(result.is_empty(), "Empty setup steps should return empty string");
506+
}
507+
508+
#[test]
509+
fn test_generate_setup_job_with_steps() {
510+
let step: serde_yaml::Value =
511+
serde_yaml::from_str("bash: echo setup").expect("valid yaml");
512+
let result = generate_setup_job(&[step], "My Agent");
513+
assert!(result.contains("SetupJob"), "Should define a SetupJob");
514+
assert!(
515+
result.contains("My Agent - Setup"),
516+
"Should include agent name in display name"
517+
);
518+
assert!(result.contains("checkout: self"), "Should include self checkout");
519+
assert!(result.contains("echo setup"), "Should include the step content");
520+
}
521+
522+
// ─── generate_teardown_job ───────────────────────────────────────────────
523+
524+
#[test]
525+
fn test_generate_teardown_job_empty_steps() {
526+
let result = generate_teardown_job(&[], "My Agent");
527+
assert!(result.is_empty(), "Empty teardown steps should return empty string");
528+
}
529+
530+
#[test]
531+
fn test_generate_teardown_job_with_steps() {
532+
let step: serde_yaml::Value =
533+
serde_yaml::from_str("bash: echo teardown").expect("valid yaml");
534+
let result = generate_teardown_job(&[step], "My Agent");
535+
assert!(result.contains("TeardownJob"), "Should define a TeardownJob");
536+
assert!(
537+
result.contains("My Agent - Teardown"),
538+
"Should include agent name in display name"
539+
);
540+
assert!(
541+
result.contains("ProcessSafeOutputs"),
542+
"Should depend on ProcessSafeOutputs"
543+
);
544+
assert!(result.contains("checkout: self"), "Should include self checkout");
545+
assert!(result.contains("echo teardown"), "Should include the step content");
546+
}
547+
}

src/fuzzy_schedule.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,38 @@ mod tests {
10361036
assert!(err.to_string().contains("at least 5 minutes"));
10371037
}
10381038

1039+
// ─── invalid hour interval error path ────────────────────────────────────
1040+
1041+
#[test]
1042+
fn test_parse_invalid_hour_interval_5h() {
1043+
let err = parse_fuzzy_schedule("every 5h").unwrap_err();
1044+
assert!(
1045+
err.to_string().contains("Valid intervals"),
1046+
"Error for 5h should mention valid intervals: {}",
1047+
err
1048+
);
1049+
}
1050+
1051+
#[test]
1052+
fn test_parse_invalid_hour_interval_7h() {
1053+
let err = parse_fuzzy_schedule("every 7h").unwrap_err();
1054+
assert!(
1055+
err.to_string().contains("not recommended"),
1056+
"Error for 7h should say 'not recommended': {}",
1057+
err
1058+
);
1059+
}
1060+
1061+
#[test]
1062+
fn test_parse_zero_hour_interval() {
1063+
let err = parse_fuzzy_schedule("every 0h").unwrap_err();
1064+
assert!(
1065+
err.to_string().contains("greater than 0"),
1066+
"Error for 0h should mention interval must be greater than 0: {}",
1067+
err
1068+
);
1069+
}
1070+
10391071
#[test]
10401072
fn test_backward_compatibility() {
10411073
// Test that simple "hourly" and "daily" still work

0 commit comments

Comments
 (0)