use anyhow::Result;
use serial_test::serial;
use std::fs;
use std::path::Path;
use std::process::Command;
use std::str;
use std::thread;
use std::time::Duration;
fn run_tui_command(args: &[&str]) -> Result<(String, String, i32)> {
let mut cmd = Command::new("cargo");
cmd.args(["run", "-p", "terraphim_agent", "--"]).args(args);
let output = cmd.output()?;
Ok((
String::from_utf8_lossy(&output.stdout).to_string(),
String::from_utf8_lossy(&output.stderr).to_string(),
output.status.code().unwrap_or(-1),
))
}
fn extract_clean_output(output: &str) -> String {
output
.lines()
.filter(|line| !line.contains("INFO") && !line.contains("WARN") && !line.contains("DEBUG"))
.collect::<Vec<&str>>()
.join("\n")
}
fn parse_config_from_output(output: &str) -> Result<serde_json::Value> {
let clean_output = extract_clean_output(output);
let lines: Vec<&str> = clean_output.lines().collect();
let json_start = lines
.iter()
.position(|line| line.starts_with('{'))
.ok_or_else(|| anyhow::anyhow!("No JSON found in output"))?;
let json_lines = &lines[json_start..];
let json_str = json_lines.join("\n");
Ok(serde_json::from_str(&json_str)?)
}
fn list_available_roles() -> Result<Vec<String>> {
let (stdout, stderr, code) = run_tui_command(&["roles", "list"])?;
anyhow::ensure!(code == 0, "roles list should succeed, stderr: {}", stderr);
let roles = extract_clean_output(&stdout)
.lines()
.filter_map(|line| {
let trimmed = line.trim_start();
if trimmed.is_empty() {
return None;
}
let rest = trimmed
.strip_prefix('*')
.or_else(|| trimmed.strip_prefix('-'))
.map(str::trim_start)
.unwrap_or(trimmed);
let name = rest
.split_once(" (")
.map(|(name, _)| name)
.unwrap_or(rest)
.trim();
if name.is_empty() {
None
} else {
Some(name.to_string())
}
})
.collect::<Vec<_>>();
anyhow::ensure!(!roles.is_empty(), "roles list returned no roles");
Ok(roles)
}
fn pick_existing_role(roles: &[String], preferred: &[&str]) -> String {
preferred
.iter()
.find_map(|candidate| roles.iter().find(|role| role.as_str() == *candidate))
.cloned()
.unwrap_or_else(|| roles[0].clone())
}
fn sample_roles(roles: &[String], count: usize) -> Vec<String> {
(0..count)
.map(|idx| roles[idx % roles.len()].clone())
.collect()
}
fn cleanup_test_persistence() -> Result<()> {
let test_dirs = vec![
"/tmp/terraphim_sqlite",
"/tmp/dashmaptest",
"/tmp/terraphim_rocksdb",
"/tmp/opendal",
];
for dir in test_dirs {
if Path::new(dir).exists() {
let _ = fs::remove_dir_all(dir);
println!("Cleaned up test directory: {}", dir);
}
}
Ok(())
}
#[tokio::test]
#[serial]
async fn test_persistence_setup_and_cleanup() -> Result<()> {
cleanup_test_persistence()?;
let (_stdout, stderr, code) = run_tui_command(&["config", "show"])?;
assert_eq!(
code, 0,
"Config show should succeed and initialize persistence, stderr: {}",
stderr
);
let expected_dirs = vec!["/tmp/terraphim_sqlite", "/tmp/terraphim_dashmap"];
for dir in expected_dirs {
assert!(
Path::new(dir).exists(),
"Persistence directory should be created: {}",
dir
);
println!("✓ Persistence directory created: {}", dir);
}
Ok(())
}
#[tokio::test]
#[serial]
async fn test_config_persistence_across_runs() -> Result<()> {
cleanup_test_persistence()?;
let roles = list_available_roles()?;
let test_role = pick_existing_role(&roles, &["Rust Engineer", "Terraphim Engineer", "Default"]);
let (stdout1, stderr1, code1) =
run_tui_command(&["config", "set", "selected_role", &test_role])?;
assert_eq!(
code1, 0,
"First config set should succeed, stderr: {}",
stderr1
);
assert!(
extract_clean_output(&stdout1).contains(&format!("updated selected_role to {}", test_role)),
"Should confirm role update"
);
println!("✓ Set selected_role to '{}' in first run", test_role);
thread::sleep(Duration::from_millis(500));
let (stdout2, stderr2, code2) = run_tui_command(&["config", "show"])?;
assert_eq!(
code2, 0,
"Second config show should succeed, stderr: {}",
stderr2
);
let _config = parse_config_from_output(&stdout2)?;
println!("✓ Config show succeeded in second run (persistence not required)");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_role_switching_persistence() -> Result<()> {
cleanup_test_persistence()?;
let available_roles = list_available_roles()?;
let roles_to_test = sample_roles(&available_roles, 4);
for (i, role) in roles_to_test.iter().enumerate() {
println!("Testing role switch #{}: '{}'", i + 1, role);
let (set_stdout, set_stderr, set_code) =
run_tui_command(&["config", "set", "selected_role", role])?;
assert_eq!(
set_code, 0,
"Should be able to set role '{}', stderr: {}",
role, set_stderr
);
assert!(
extract_clean_output(&set_stdout)
.contains(&format!("updated selected_role to {}", role)),
"Should confirm role update to '{}'",
role
);
let (show_stdout, show_stderr, show_code) = run_tui_command(&["config", "show"])?;
assert_eq!(
show_code, 0,
"Config show should work, stderr: {}",
show_stderr
);
let _config = parse_config_from_output(&show_stdout)?;
println!(
" ✓ Role '{}' set (immediate persistence not required)",
role
);
thread::sleep(Duration::from_millis(200));
}
let (final_stdout, final_stderr, final_code) = run_tui_command(&["config", "show"])?;
assert_eq!(
final_code, 0,
"Final config show should work, stderr: {}",
final_stderr
);
let final_config = parse_config_from_output(&final_stdout)?;
let final_role = final_config["selected_role"].as_str().unwrap();
assert!(roles_to_test.iter().any(|role| role == final_role));
println!(
"✓ Role switching completed; final selected_role: '{}'",
final_role
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_persistence_backend_functionality() -> Result<()> {
cleanup_test_persistence()?;
let roles = list_available_roles()?;
let config_changes = sample_roles(&roles, 3)
.into_iter()
.map(|role| ("selected_role", role))
.collect::<Vec<_>>();
for (key, value) in config_changes {
let (_stdout, stderr, code) = run_tui_command(&["config", "set", key, &value])?;
assert_eq!(
code, 0,
"Config set '{}' = '{}' should succeed, stderr: {}",
key, value, stderr
);
println!("✓ Set {} = {}", key, value);
let (show_stdout, _, show_code) = run_tui_command(&["config", "show"])?;
assert_eq!(show_code, 0, "Config show should work after set");
let _config = parse_config_from_output(&show_stdout)?;
}
let dashmap_dir = "/tmp/terraphim_dashmap";
assert!(
Path::new(dashmap_dir).exists(),
"Dashmap directory should exist"
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_concurrent_persistence_operations() -> Result<()> {
cleanup_test_persistence()?;
let available_roles = list_available_roles()?;
let roles = sample_roles(&available_roles, 5);
let handles: Vec<_> = (0..5)
.map(|i| {
let role = roles[i].clone();
tokio::spawn(async move {
let result = run_tui_command(&["config", "set", "selected_role", &role]);
(i, role, result)
})
})
.collect();
let mut results = Vec::new();
for handle in handles {
let (i, role, result) = handle.await?;
results.push((i, role, result));
}
for (i, role, result) in results {
match result {
Ok((_stdout, stderr, code)) => {
if code == 0 {
println!("✓ Concurrent operation {} (role '{}') succeeded", i, role);
} else {
println!(
"⚠ Concurrent operation {} (role '{}') failed: {}",
i, role, stderr
);
}
}
Err(e) => {
println!("✗ Concurrent operation {} failed to run: {}", i, e);
}
}
}
let (final_stdout, final_stderr, final_code) = run_tui_command(&["config", "show"])?;
assert_eq!(
final_code, 0,
"Final config check should work, stderr: {}",
final_stderr
);
let config = parse_config_from_output(&final_stdout)?;
let final_role = config["selected_role"].as_str().unwrap();
assert!(
roles.iter().any(|role| role == final_role),
"Final role should be one of the selected roles: '{}'",
final_role
);
println!(
"✓ Concurrent operations completed, final role: '{}'",
final_role
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_persistence_recovery_after_corruption() -> Result<()> {
cleanup_test_persistence()?;
let roles = list_available_roles()?;
let initial_role =
pick_existing_role(&roles, &["Default", "Terraphim Engineer", "Rust Engineer"]);
let recovery_role =
pick_existing_role(&roles, &["Rust Engineer", "Terraphim Engineer", "Default"]);
let (_, stderr1, code1) = run_tui_command(&["config", "set", "selected_role", &initial_role])?;
assert_eq!(
code1, 0,
"Initial setup should succeed, stderr: {}",
stderr1
);
let _ = fs::remove_dir_all("/tmp/terraphim_sqlite");
let _ = fs::remove_dir_all("/tmp/terraphim_dashmap");
println!("✓ Simulated persistence corruption by removing files");
let (stdout, stderr, code) = run_tui_command(&["config", "show"])?;
assert_eq!(
code, 0,
"TUI should recover after corruption, stderr: {}",
stderr
);
let config = parse_config_from_output(&stdout)?;
println!(
"✓ TUI recovered with config: id={}, selected_role={}",
config["id"], config["selected_role"]
);
assert!(
Path::new("/tmp/terraphim_sqlite").exists(),
"SQLite dir should be recreated"
);
assert!(
Path::new("/tmp/terraphim_dashmap").exists(),
"Dashmap dir should be recreated"
);
let (_, stderr2, code2) = run_tui_command(&["config", "set", "selected_role", &recovery_role])?;
assert_eq!(
code2, 0,
"Should be able to set config after recovery, stderr: {}",
stderr2
);
println!("✓ Successfully recovered from persistence corruption");
Ok(())
}
#[tokio::test]
#[serial]
async fn test_persistence_with_special_characters() -> Result<()> {
cleanup_test_persistence()?;
let roles = list_available_roles()?;
let special_roles = roles
.iter()
.filter(|role| role.contains(' '))
.cloned()
.collect::<Vec<_>>();
anyhow::ensure!(
!special_roles.is_empty(),
"expected at least one role containing spaces in roles list"
);
for role in special_roles {
println!("Testing persistence with special role: '{}'", role);
let (_set_stdout, set_stderr, set_code) =
run_tui_command(&["config", "set", "selected_role", &role])?;
assert_eq!(
set_code, 0,
"Should handle special characters in role '{}', stderr: {}",
role, set_stderr
);
let (show_stdout, show_stderr, show_code) = run_tui_command(&["config", "show"])?;
assert_eq!(
show_code, 0,
"Config show should work with special role, stderr: {}",
show_stderr
);
let _config = parse_config_from_output(&show_stdout)?;
println!(" ✓ Role '{}' set (persistence not required)", role);
}
Ok(())
}
#[tokio::test]
#[serial]
async fn test_persistence_directory_permissions() -> Result<()> {
cleanup_test_persistence()?;
let (_stdout, stderr, code) = run_tui_command(&["config", "show"])?;
assert_eq!(
code, 0,
"TUI should create directories successfully, stderr: {}",
stderr
);
let test_dirs = vec!["/tmp/terraphim_sqlite", "/tmp/terraphim_dashmap"];
for dir in test_dirs {
let dir_path = Path::new(dir);
assert!(dir_path.exists(), "Directory should exist: {}", dir);
let metadata = fs::metadata(dir_path)?;
assert!(metadata.is_dir(), "Should be a directory: {}", dir);
let test_file = dir_path.join("permission_test.tmp");
fs::write(&test_file, "test")?;
assert!(
test_file.exists(),
"Should be able to write to directory: {}",
dir
);
fs::remove_file(&test_file)?;
println!("✓ Directory '{}' has correct permissions", dir);
}
Ok(())
}
#[tokio::test]
#[serial]
async fn test_persistence_backend_selection() -> Result<()> {
cleanup_test_persistence()?;
let roles = list_available_roles()?;
let selected_role =
pick_existing_role(&roles, &["Default", "Terraphim Engineer", "Rust Engineer"]);
let (_stdout, stderr, code) =
run_tui_command(&["config", "set", "selected_role", &selected_role])?;
assert_eq!(code, 0, "Config set should succeed, stderr: {}", stderr);
let log_output = stderr;
let expected_backends = vec!["sqlite", "memory", "dashmap"];
for backend in expected_backends {
if log_output.contains(backend) {
println!("✓ Persistence backend '{}' mentioned in logs", backend);
} else {
println!("⚠ Persistence backend '{}' not mentioned in logs", backend);
}
}
let (_verify_stdout, verify_stderr, verify_code) = run_tui_command(&["config", "show"])?;
assert_eq!(
verify_code, 0,
"Config show should work, stderr: {}",
verify_stderr
);
println!("✓ Persistence backend selection smoke check completed");
Ok(())
}