use ta_changeset::draft_package::ValidationEntry;
#[test]
fn validation_entry_round_trip() {
let entry = ValidationEntry {
command: "echo validation-ok".to_string(),
exit_code: 0,
duration_secs: 1,
stdout_tail: "validation-ok".to_string(),
};
let json = serde_json::to_string(&entry).unwrap();
let back: ValidationEntry = serde_json::from_str(&json).unwrap();
assert_eq!(back.command, "echo validation-ok");
assert_eq!(back.exit_code, 0);
assert_eq!(back.duration_secs, 1);
assert_eq!(back.stdout_tail, "validation-ok");
}
#[test]
fn validation_entry_failure() {
let entry = ValidationEntry {
command: "cargo test".to_string(),
exit_code: 101,
duration_secs: 47,
stdout_tail: "FAILED: test_foo\n1 test failed".to_string(),
};
assert_ne!(entry.exit_code, 0);
let json = serde_json::to_string(&entry).unwrap();
let back: ValidationEntry = serde_json::from_str(&json).unwrap();
assert_eq!(back.exit_code, 101);
assert!(back.stdout_tail.contains("FAILED"));
}
#[test]
fn draft_package_empty_validation_log_skipped_in_json() {
let entries: Vec<ta_changeset::draft_package::ValidationEntry> = vec![];
let json = serde_json::to_string(&entries).unwrap();
assert_eq!(json, "[]", "empty vec serializes to []");
let entry = ta_changeset::draft_package::ValidationEntry {
command: "echo test".to_string(),
exit_code: 0,
duration_secs: 1,
stdout_tail: "test".to_string(),
};
let entries_with_item = vec![entry];
let json_with = serde_json::to_string(&entries_with_item).unwrap();
assert!(
json_with.contains("echo test"),
"non-empty vec serializes correctly"
);
assert!(!entries_with_item.is_empty());
}
#[test]
fn draft_package_validation_log_round_trip() {
let entries = vec![
ta_changeset::draft_package::ValidationEntry {
command: "echo ok".to_string(),
exit_code: 0,
duration_secs: 0,
stdout_tail: "ok".to_string(),
},
ta_changeset::draft_package::ValidationEntry {
command: "cargo build".to_string(),
exit_code: 1,
duration_secs: 42,
stdout_tail: "error[E0308]: type mismatch".to_string(),
},
];
let json = serde_json::to_string_pretty(&entries).unwrap();
assert!(json.contains("echo ok"));
assert!(json.contains("cargo build"));
assert!(json.contains("type mismatch"));
let back: Vec<ta_changeset::draft_package::ValidationEntry> =
serde_json::from_str(&json).unwrap();
assert_eq!(back.len(), 2);
assert_eq!(back[0].command, "echo ok");
assert_eq!(back[0].exit_code, 0);
assert_eq!(back[1].exit_code, 1);
assert!(back[1].stdout_tail.contains("type mismatch"));
let has_failures = back.iter().any(|e| e.exit_code != 0);
assert!(has_failures, "should detect failed checks");
}
#[test]
fn validation_log_has_failures_detection() {
let passing = [
ValidationEntry {
command: "echo a".to_string(),
exit_code: 0,
duration_secs: 0,
stdout_tail: "a".to_string(),
},
ValidationEntry {
command: "echo b".to_string(),
exit_code: 0,
duration_secs: 0,
stdout_tail: "b".to_string(),
},
];
assert!(!passing.iter().any(|e| e.exit_code != 0));
let with_failure = [
ValidationEntry {
command: "echo ok".to_string(),
exit_code: 0,
duration_secs: 0,
stdout_tail: "ok".to_string(),
},
ValidationEntry {
command: "cargo test".to_string(),
exit_code: 1,
duration_secs: 30,
stdout_tail: "FAILED".to_string(),
},
];
assert!(with_failure.iter().any(|e| e.exit_code != 0));
let failed: Vec<&str> = with_failure
.iter()
.filter(|e| e.exit_code != 0)
.map(|e| e.command.as_str())
.collect();
assert_eq!(failed, vec!["cargo test"]);
}
use std::path::PathBuf;
use std::process::{Child, Command};
use std::time::{Duration, Instant};
struct DaemonHandle {
child: Child,
config_dir: PathBuf,
socket_path: PathBuf,
}
impl DaemonHandle {
fn start() -> Option<Self> {
let tmp = tempfile::tempdir().ok()?;
let config_dir = tmp.path().to_path_buf();
std::mem::forget(tmp);
let socket_path = config_dir.join("daemon.sock");
let daemon_bin = std::env::current_exe()
.ok()
.and_then(|p| {
let mut d = p.parent()?.to_path_buf();
for _ in 0..8 {
for candidate in &[
d.join("ta-daemon"),
d.join("debug").join("ta-daemon"),
d.join("release").join("ta-daemon"),
] {
if candidate.exists() {
return Some(candidate.clone());
}
}
match d.parent() {
Some(p) => d = p.to_path_buf(),
None => break,
}
}
None
})
.or_else(|| {
std::env::var_os("PATH").and_then(|paths| {
std::env::split_paths(&paths)
.map(|p| p.join("ta-daemon"))
.find(|p| p.exists())
})
})?;
let child = Command::new(&daemon_bin)
.arg("--config-dir")
.arg(&config_dir)
.arg("--socket")
.arg(&socket_path)
.spawn()
.ok()?;
let handle = DaemonHandle {
child,
config_dir,
socket_path: socket_path.clone(),
};
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if socket_path.exists() {
return Some(handle);
}
std::thread::sleep(Duration::from_millis(200));
}
Some(handle)
}
fn is_ready(&self) -> bool {
self.socket_path.exists()
}
}
impl Drop for DaemonHandle {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
#[test]
#[ignore]
fn test_draft_validation_log_e2e() {
let handle = match DaemonHandle::start() {
Some(h) if h.is_ready() => h,
_ => {
eprintln!("SKIP: ta-daemon not available or did not start in time");
return;
}
};
assert!(
handle.socket_path.exists(),
"daemon socket should exist after start"
);
let workflow_toml = r#"
[workflow]
name = "validation-test"
[validation]
required_checks = ["echo validation-ok"]
"#;
let workflow_path = handle.config_dir.join("validation-test.toml");
std::fs::write(&workflow_path, workflow_toml).expect("write workflow");
assert!(workflow_path.exists(), "workflow config should exist");
let parsed: toml::Value = toml::from_str(workflow_toml).expect("workflow toml should parse");
assert!(
parsed.get("validation").is_some(),
"validation section should exist"
);
println!(
"E2E test_draft_validation_log_e2e: daemon started at {:?}, validation workflow valid \
— full test requires MCP client",
handle.socket_path
);
}
#[test]
#[ignore]
fn test_dependency_graph_e2e() {
let handle = match DaemonHandle::start() {
Some(h) if h.is_ready() => h,
_ => {
eprintln!("SKIP: ta-daemon not available or did not start in time");
return;
}
};
assert!(
handle.socket_path.exists(),
"daemon socket should exist after start"
);
let workflow_toml = r#"
[workflow]
name = "dep-graph-test"
[[steps]]
id = "step-1"
title = "First step"
goal = "echo step-one-done"
[[steps]]
id = "step-2"
title = "Second step (depends on step-1)"
goal = "echo step-two-done"
depends_on = ["step-1"]
"#;
let workflow_path = handle.config_dir.join("dep-test.toml");
std::fs::write(&workflow_path, workflow_toml).expect("write workflow");
assert!(workflow_path.exists(), "workflow config should exist");
let parsed: toml::Value = toml::from_str(workflow_toml).expect("workflow toml should parse");
let steps = parsed.get("steps").and_then(|v| v.as_array());
assert!(steps.is_some(), "steps array should exist");
let steps = steps.unwrap();
assert_eq!(steps.len(), 2, "should have two steps");
let step2_deps = steps[1].get("depends_on").and_then(|v| v.as_array());
assert!(step2_deps.is_some(), "step-2 should have depends_on");
assert_eq!(
step2_deps.unwrap().len(),
1,
"step-2 should depend on exactly one step"
);
println!(
"E2E test_dependency_graph_e2e: daemon started, two-step dependency workflow valid \
— full ordering test requires MCP client"
);
}
#[test]
#[ignore]
fn test_ollama_agent_mock_e2e() {
let handle = match DaemonHandle::start() {
Some(h) if h.is_ready() => h,
_ => {
eprintln!("SKIP: ta-daemon not available or did not start in time");
return;
}
};
assert!(
handle.socket_path.exists(),
"daemon socket should exist after start"
);
let mock_response = serde_json::json!({
"model": "llama3.2",
"message": {
"role": "assistant",
"content": "I will complete the task.",
"tool_calls": []
},
"done": true
});
let mock_response_str = serde_json::to_string(&mock_response).unwrap();
assert!(
mock_response_str.contains("\"done\":true"),
"mock response should contain done:true"
);
assert!(
mock_response_str.contains("llama3.2"),
"mock response should reference the model"
);
println!(
"E2E test_ollama_agent_mock_e2e: daemon started at {:?}, mock fixture valid \
— full test requires MCP client + mock HTTP server on localhost:11434",
handle.socket_path
);
}